Files

555 lines
24 KiB
Markdown
Raw Permalink Normal View History

<!-- retoor <retoor@molodetz.nl> -->
# Content master handover: Molodetz
Audience: the coworker agent that takes over **content** for production
Molodetz. This is not an engineering or DPP-template brief.
| Fact | Value |
|------|-------|
| Production host | `https://molodetz.nl` |
| Staging host | `https://staging.app.molodetz.nl` (proxies `127.0.0.1:19847`) |
| Test host | `https://molodetz-test.app.molodetz.nl` (proxies `127.0.0.1:19849`) |
| Local/dev | `http://127.0.0.1:8088` (`make dev`) |
| Git remote | `https://retoor.molodetz.nl/retoor/ad.git` (private Gitea) |
| Local clone | `/workspace/retoor/molodetz-app` |
| Package | `molodetz` |
| Language on site | English only |
| Owner / sole public author today | `retoor` |
| Old brand | Absurd Development (git repo name `ad`; public product name Molodetz) |
`DEVELOPMENT_FLOW.md` in this repo is the multi-agent **engineering lesson
log** that may feed the DPP template. It is **not** the content bible.
Content work stays here and on the live site. Do not fold Molodetz-specific
voice, memes, or intro text into `/workspace/dpp/dptemplate.md`.
Production admin password and `SECRET_KEY` live with the operator / secrets
store (compose `PROD_*` env). **Never** put production passwords, API keys,
or `.env` values in this file, in posts, in captions, or in git.
## 0. Repository and code changes
Private Gitea repository:
```
https://retoor.molodetz.nl/retoor/ad.git
```
- Clone, commit, and push code (gallery wiring, captions in `gallery.py`,
generator scripts if promoted into the repo) against that remote on
branch `main` unless ordered otherwise.
- Auth for git: Gitea user credentials from the **operator secrets store**
(same class of secret as `~/.config/gitea/retoor.molodetz.nl.env` on the
build box). Use a one-shot credential helper or `GIT_ASKPASS`; **never**
embed the password in the remote URL, in `.git/config`, in this file, or
in chat logs.
- Content-only work that only needs admin UI/API does not require a push.
Changing published gallery files without updating `GALLERY` in git will
drift; prefer commit+push for media registry changes.
- Markdown files (including this one) need explicit user confirmation
before commit.
## 0.1 Machine-readable docs for agents
Before calling write APIs, load the live docs so samples stay in sync with
the registry (`molodetz/docs_api/*`, built by `molodetz/docs_export.py`):
| Doc | Production URL | Notes |
|-----|----------------|-------|
| Docs home | `https://molodetz.nl/docs` | Sidebar + prose |
| API groups (HTML) | `https://molodetz.nl/docs/api/content`, `.../join`, `.../account`, `.../admin` | Admin group requires an admin session |
| Full download (markdown) | `https://molodetz.nl/docs/download.md` | Auto-generated: every endpoint with method, path, auth, fields, curl, request JSON, response sample |
| Full download (HTML) | `https://molodetz.nl/docs/download.html` | Same content, HTML |
| OpenAPI | `https://molodetz.nl/swagger` | Framework OpenAPI UI (`/openapi.json`) |
| Auth prose | `https://molodetz.nl/docs/api` | How sessions and API keys work |
Replace the host with staging or test URLs when working on those
environments. Downloads omit the Admin group unless the request is
authenticated as admin.
### Auth (Account group)
No JWT. No OAuth. Every HTML route also returns JSON when called with
`Accept: application/json`.
1. **Session cookie** (64 hex): `POST /auth/login` with
`username`, `password`, optional `remember`. Cookie lasts 7 days
(30 with remember). Sample success:
`{"ok": true, "redirect": "/admin", "data": null}`.
2. **`X-API-KEY`**: after login, `GET /profile/api-key` (member+). Sample
shape `{"api_key": "..."}`. Destructive renew:
`POST /profile/api-key/regenerate`.
3. Also accepted by the app (see docs prose): `Authorization: Bearer` and
`Basic` with the same credential resolution as the API key path.
Log out: `POST /auth/logout`. Accept terms if gated:
`POST /terms/accept` (member).
### Public read (Content group)
Use these to verify what the site shows after you publish. All `auth=public`.
| Method | Path | Purpose | Response sample (registry) |
|--------|------|---------|----------------------------|
| GET | `/` | Home + featured flyer counts | `{"posts": [], "flyer_count": 4, "meme_count": 22}` |
| GET | `/roll` | Roll feed; query `before` cursor | `{"title": "Roll", "topic": "roll", "posts": [], "next_cursor": null}` |
| GET | `/standard` | Standard feed | `{"title": "Standard", "topic": "standard", "posts": []}` |
| GET | `/posts/{slug}` | One post | `{"post": {"title": "...", "topic": "roll"}, "author": {"username": "retoor"}}` |
| GET | `/flyers` | Flyer gallery | `{"kind": "flyer", "title": "Flyers", "items": []}` |
| GET | `/memes` | Meme gallery | `{"kind": "meme", "title": "Memes", "items": []}` |
| GET | `/people` | People list | `{"people": [{"username": "retoor", "post_count": 5}]}` |
| GET | `/people/{username}` | Profile + posts | `{"person": {"username": "retoor"}, "posts": []}` |
| GET | `/health` | Liveness | `{"status": "ok", "version": "..."}` |
There is **no** public or admin HTTP API to upload a new meme/flyer file.
Galleries are files under `molodetz/static/media/` plus rows in
`molodetz/gallery.py` synced on boot. Content master still uses Content
GET to confirm captions and order after deploy.
Example:
```bash
curl -H 'Accept: application/json' https://molodetz.nl/roll
curl -H 'Accept: application/json' https://molodetz.nl/flyers
```
### Join group (visitor intake; admin triage separately)
| Method | Path | Auth | Body / notes |
|--------|------|------|--------------|
| GET | `/join` | public | Form; sample `{"submitted": false}` |
| POST | `/join` | public | Required `name`, `contact`; optional `repo_url`, `message`. Sample: `{"ok": true, "redirect": "/join?ok=1", "data": {"uid": "0190..."}}` |
Join rows are **never public**. Content master does not invent join
stories. Admins list and update them under Admin (below).
### Admin group (content master write path)
Requires admin role (retoor). Prefer session cookie after UI login, or
admin API key via `X-API-KEY`. Always send `Accept: application/json`
for machine clients.
#### Posts
| Method | Path | Purpose | Body fields (registry) |
|--------|------|---------|------------------------|
| GET | `/admin/posts` | List | |
| GET | `/admin/posts/new` | New form | |
| POST | `/admin/posts` | Create | required `title`, `body`; `topic` example `roll` (also `standard`); `status` example `draft` (`published`); optional `is_placeholder` |
| GET | `/admin/posts/{uid}/edit` | Edit form | path `uid` |
| POST | `/admin/posts/{uid}` | Save | path `uid` + same fields as create |
| POST | `/admin/posts/{uid}/publish` | Toggle publish | path `uid` |
| POST | `/admin/posts/{uid}/delete` | Soft delete | path `uid` |
Create sample response:
`{"ok": true, "redirect": "/admin/posts", "data": {"uid": "..."}}`.
Example create (password never stored here; use operator secrets):
```bash
curl -X POST https://molodetz.nl/admin/posts \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H "X-API-KEY: $MOLODETZ_API_KEY" \
-d '{"title":"A title","body":"Text in **markdown**.","topic":"roll","status":"draft"}'
```
#### Home intro (settings)
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/admin/settings` | Read settings including `site_intro` |
| POST | `/admin/settings` | Save settings (form/JSON; include `site_intro` markdown) |
#### Join triage
| Method | Path | Purpose | Body |
|--------|------|---------|------|
| GET | `/admin/joins` | List requests | |
| POST | `/admin/joins/{uid}/status` | Set status | `status` in `open`, `contacted`, `accepted`, `declined` |
| POST | `/admin/joins/{uid}/check` | Fetch repo link title via guarded outbound client | |
#### Trash (restore mistaken deletes)
| Method | Path | Body |
|--------|------|------|
| GET | `/admin/trash` | |
| POST | `/admin/trash/restore` | required `stamp` |
| POST | `/admin/trash/purge` | required `stamp` (destructive) |
Other admin endpoints (services, backups, users, audit, stats) are
operations, not day-to-day content. See `/docs/api/admin` or
`download.md` while logged in as admin for the full list. Do not schedule
backups or purge trash unless ordered.
### Source of truth
Endpoint tables above are copied from `molodetz/docs_api/content.py`,
`join.py`, `account.py`, and `admin.py` as of this handover. If the
registry changes, **trust live** `https://molodetz.nl/docs/download.md`
over this file and update the handover when you next touch it.
---
## 1. What the site is built for
Molodetz is a **community blog roll**: calm elite serif UI, slightly
provocative content, markdown posts in SQLite, two galleries (flyers and
memes), a people list, and a real join form.
Purpose, as stated in the home intro (`site_intro`, seeded from
`molodetz/seed/intro.md`) and in `SELECTION.md`:
- Hostile, data-backed elite intro: floor = current skill; the site does
not flatter mediocrity.
- Help people **reach elite level**, especially by learning to use AI as a
craft tool, not as a substitute for judgement.
- Divide honestly: classic hand-coders who refuse AI, vibe "coders" who
refuse to learn to code, and the **elite twice** path (classic craft +
AI).
- Offer a path: take the profession seriously; ship; read every line the
machine writes.
- Side product context in the intro: a development environment for the
elite (vim/tmux-class toolkit plus AI skill). Do not invent product
claims beyond what retoor publishes.
`SELECTION.md` one-liner: "Calm elite serif community blog roll for
retoor. English. Posts, flyers, memes, people, join intent."
---
## 2. Intentions
- Fight mediocrity. A pass is the floor, not an ambition (seed roll post
and retired Dutch assets said the same thing; English site keeps it).
- Divide and offer a path. Look at the person next to you: replaced or
replacing.
- Promote **elite programmers who embraced AI** and kept craft:
"elite twice" (know the code, use the machine, read every line, ship).
- Roast **classic programmers who refuse AI** on principle (vim/tmux
purity, "I don't need autocomplete", compilers-made-us-lazy recycling).
- Roast **vibe coders** who refuse to learn to code / do not take the
profession seriously (prompt-and-pray, tab-tab-accept-all, README with
badges and zero tests, course before craft).
- Facts over feelings. Pride backed by data is allowed; apology spirals
are not the brand.
---
## 3. Tone
| Do | Do not |
|----|--------|
| Harsh, rude, elitist when earned | Soft HR copy, engagement bait |
| Facts, never invented lies | Fake testimonials, fake members |
| Based, conservative craft ethos | Hype, hustle-culture emptiness |
| Pro-AI **and** pro classic elite; especially the combo | Pro-AI that excuses ignorance of code |
| Proud and specific | God-complex apology spiral |
| English | Dutch on the public site |
| Short punchy posts and one-point flyers | Em-dashes, emoji decoration, slurs |
Banned characters in shipped prose and captions: em dash (U+2014) and
en dash (U+2013). Use commas, periods, semicolons, or hyphens.
---
## 4. What NOT to do
- Invent members, join success stories, or co-authors. People today is
effectively **retoor only** until real people join and are approved.
- Selfie / photo of retoor as gallery art.
- DevPlace, Nigel, or other named private individuals in memes or posts.
- JWT, OAuth-as-flex, framework catalogue flex, payment/ad SDK vibes in
content (engineering also forbids JWT; content should not romanticize
it).
- Dutch copy on public pages, captions, or new assets. Old Dutch media
files may remain on disk under `RETIRED_GALLERY` but must stay
unpublished.
- Em-dashes in UI copy, posts, captions, alt text, or image text.
- Commit secrets, `.env`, production passwords, API keys, or dump them
into CONTENT docs.
- Weaken factual claims in the intro without retoor's order. Do not
"tone down" the hostile frame into a friendly corporate blog unless
ordered.
- Fold Molodetz voice into the DPP template or treat `DEVELOPMENT_FLOW.md`
as a place to store meme recipes.
---
## 5. Content surfaces
| Surface | URL | What it is | Who edits |
|---------|-----|------------|-----------|
| Home intro | `/` | `site_intro` setting (markdown). Seed: `molodetz/seed/intro.md`. Hostile elite manifesto. | Admin Settings |
| Roll | `/roll` | Short notes, newest first. Topic `roll`. Seed placeholders exist. | Admin Posts |
| Standard | `/standard` | Standards / taste posts. Topic `standard`. Seed: "Thirty-one choices. You have zero." (DPP-ish stack choices, no JWT, no JS framework). | Admin Posts |
| Flyers | `/flyers` | Portrait gallery. Kind `flyer`. Featured flyer on home = first flyer by gallery position (`flyer-elite-twice.jpg`). | Files + `molodetz/gallery.py` sync |
| Memes | `/memes` | Square gallery. Kind `meme`. Classic / vibe / elite sets + a few older English leftovers. | Files + gallery sync |
| People | `/people`, `/people/retoor` | Authors with posts. | Real accounts only; do not invent |
| Join | `/join` | "I'm in" form; stored for admins, never public. | Visitors submit; admin triage at `/admin/joins` |
| Docs | `/docs` | Operator/API docs. Not a content marketing channel. Contributing/component docs were removed on purpose. | Engineering |
### Seed posts (English; from `molodetz/content.py`)
| Topic | Title | Role |
|-------|-------|------|
| standard | Thirty-one choices. You have zero. | Stack / taste standard |
| roll | A pass is not an ambition. | Floor / mediocrity |
| roll | Finished is a feature. | Ship |
| roll | The assistant is not a senior. | AI without judgement |
| roll | No course. | Anti weekend-ebook path |
Roll seeds are created as **placeholders** (`is_placeholder`); bodies may
carry a carried-over note. Prefer replacing placeholders with real retoor
stories over deleting the point.
### Live gallery (published English set)
**Flyers** (1080x1350), order matters (home featured = first):
1. `flyer-elite-twice.jpg` - Elite twice.
2. `flyer-taste.jpg` - Taste does not autocomplete.
3. `flyer-demo.jpg` - A demo is not a product.
4. `flyer-hands.jpg` - Your hands are not the craft.
**Memes** (1080x1080): six classic, six vibe, six elite (`meme-classic-*`,
`meme-vibe-*`, `meme-elite-*`), plus older English leftovers
`meme-compiles.jpg`, `meme-senior.jpg`, `meme-trophy.jpg`,
`meme-todo.jpg` (legacy 1280x720 era; keep or retire only on order).
**Retired on disk, unpublished** (`RETIRED_GALLERY` in `gallery.py`): old
Dutch flyers/memes/posters/takes (`flyer-keuzes.jpg`, `meme-vloer.jpg`,
`poster-*`, `take-*`, `story.jpg`, ...). Soft-deleted in DB. Do not
re-publish.
Captions/alt live in `GALLERY` tuples and sync into `media_items` on boot.
English only. Captions must not start with SQL keywords that trip
sqlglot string lint (`SELECT`, `INSERT`, ...).
---
## 6. How to publish
Prefer the admin HTML UI for interactive work. Prefer the Admin API
(section 0.1) for scripts and agent automation. Both share the same
handlers.
### Login
1. Open `https://molodetz.nl/auth/login` (or local/staging equivalent).
2. Username: `retoor` (production admin username from env bootstrap).
3. Password: **production admin password is with the operator / secrets
store, never in the repo.** Staging/test may use compose defaults
documented in `README.md`; production uses `PROD_ADMIN_PASSWORD`.
4. Session cookie, 7 days (30 with remember). No JWT.
### Edit home intro
1. `/admin/settings`
2. Field `site_intro` (markdown textarea).
3. Save. Home re-renders from settings (landing cache TTL applies).
### Create / edit / publish a post
1. `/admin/posts` or `/admin/posts/new`
2. Title, markdown body, topic `roll` or `standard`, status draft or
published.
3. Publish/unpublish from the posts list.
4. Soft delete goes to trash; restore from `/admin/trash` if needed.
### Join requests
1. `/admin/joins` for open requests.
2. Status and optional repo-link check (outbound stealth client).
3. Do not invent acceptances in public content.
### Galleries (flyers / memes)
1. Add JPEG under `molodetz/static/media/` with an English filename.
2. Register in `GALLERY` in `molodetz/gallery.py` with kind + English
caption (alt text).
3. Put new flyers in the desired order; index 0 is the home featured
flyer.
4. Deploy / restart so `sync_gallery()` runs (bootstrap). Dedupe uses
perceptual hash distance `<= 2`; near-duplicate layouts are skipped.
5. Soft-retire old files via `RETIRED_GALLERY` + `retire_media`; keep
bytes on disk unless ordered otherwise.
6. Visual-check every new image (open the PNG/JPG; spelling, crop,
legibility). Build a contact sheet when shipping a set
(`out/memes-sheet.jpg` practice).
Engineering owns compose, molohttp, and data trees
(`./data/production` is holy). Content master does not refresh staging
from production or touch molohttp unless explicitly ordered.
---
## 7. Meme / flyer production recipe
Generator scratch (not committed):
`/workspace/tmp/molodetz-app-20261005/memes/`
(`lib.py`, `parts.py`, `classic.py`, `vibe.py`, `elite.py`, `flyers.py`,
`gen.py`). Prefer regenerating with Pillow so **all text is drawn**, never
baked into a copyrighted meme photo.
### Dimensions and output
| Kind | Size | Format |
|------|------|--------|
| Meme | 1080 x 1080 | JPEG, quality ~93, subsampling 0 (`lib.save`) |
| Flyer | 1080 x 1350 | same |
### Fonts (from `lib.py` `FONT_FILES`)
Base dirs:
- Google: `/usr/share/fonts/truetype/sand-box/google/`
- Custom: `/usr/share/fonts/truetype/sand-box/custom/`
- DejaVu: `/usr/share/fonts/truetype/dejavu/`
| Key | File | Typical use |
|-----|------|-------------|
| `anton` | Anton/Anton-Regular.ttf | Meme titles, impact headers |
| `archivo` | Archivo Black/ArchivoBlack-Regular.ttf | Dense meme body |
| `mono` / `monob` / `monosb` | IBM Plex Mono Regular/Bold/SemiBold | Terminals, keys |
| `space` | Space Mono/SpaceMono-Bold.ttf | **wordmark `molodetz`** |
| `serif` / `serifi` | DM Serif Display Regular/Italic | Flyer headlines |
| `caslon` / `caslonb` / `casloni` | Libre Caslon Text | Flyer body / italics |
| `sans` / `sanssb` / `sansb` / `sansblack` | Source Sans Pro | UI labels, flyer header |
| `sym` / `symb` | DejaVu Sans | Glyphs (e.g. crown) |
| `marker` | Permanent Marker | Occasional accent |
Text fitting: `block()` / `fit()` must succeed; never ship cropped
letters. Uppercase via `upper=True` when the layout calls for it.
### Wordmark
- Literal text: `molodetz` (lowercase).
- Font: Space Mono Bold, default size **24**, pad **30**.
- Default corner: **bottom-right** (`corner="br"`); flyers draw bottom-right
with ink; some dark memes use a dark plate behind the mark or
bottom-left when a layout conflicts.
- Alpha ~170-230 depending on background contrast.
### Flyer palette and layout (`flyers.py`)
- Paper cream `PAPER = (243, 238, 227)`
- Ink `(28, 26, 24)`, muted `(110, 102, 92)`, accent red `(178, 48, 36)`
- Margin `M = 96`
- Header: spaced `MOLODETZ` (Source Sans Bold 26) + `No. N of 4` (Caslon italic)
- Motif, then DM Serif headline, short red rule, Caslon body
- Footer: `ONE POINT PER SHEET` + wordmark
- One point per sheet. Calm but cutting. English.
Current four points: elite twice; taste does not autocomplete; a demo is
not a product; your hands are not the craft.
### Meme palettes and targets
Roughly one third each:
1. **Classic (refuse AI)** - dark terminals, cream starter packs, charts
where keystrokes rise and features stay flat, certificates of manual
labour, multi-generation refusal. Colors: near-black gradients, cold
greys, warning red `(255, 95, 86)`, occasional yellow band.
2. **Vibe (refuse craft)** - Win95-style prompt-and-pray, pink/purple
galaxy "vibe debugging", starter packs (TAB, sk-live-, localhost),
weekend-to-course arcs, Drake-style NAH/YEP priorities.
3. **Elite (embrace AI + craft)** - gold `GOLD = (255, 196, 60)`, dark
navy/black panels, crown glyph, build logs that review every line,
choose-your-fighter with elite column highlighted, curves that keep
climbing, achievement unlocked.
Classic formats allowed as **layouts only** (two-panel, galaxy brain,
Drake approve/reject, starter pack, fake terminal, fake error, fake
chart, certificate). Draw backgrounds programmatically or with
non-copyrighted generation. No stolen meme template photos.
Dev-culture references encouraged: vim/tmux, Stack Overflow, works on my
machine, README-driven, 10x, git blame, compiler errors, prompt-and-pray,
tab-tab-tab accept-all, punch cards, etc.
### Content bans on assets
No slurs. No real named individuals. No em-dashes in image text. No
DevPlace/Nigel. No selfie. English filenames and English captions.
### Wiring and QA
1. Generate into scratch `out/`, visual-read every file, fix spelling.
2. Copy into `molodetz/static/media/`.
3. Update `GALLERY` captions; retire Dutch leftovers in `RETIRED_GALLERY`.
4. Contact sheet of the full new set (filenames under each tile).
5. Restart / sync; confirm `/flyers`, `/memes`, home featured flyer.
6. Keep pairwise phash distance clearly above 2 so sync does not drop
"duplicates".
---
## 8. Consistency checklist (every new asset or post)
- [ ] English only; no Dutch UI copy.
- [ ] No em-dash or en-dash characters.
- [ ] Tone matches section 3 (harsh when earned, factual, pro elite+AI).
- [ ] Targets the right enemy or the elite path; does not punch down with
slurs or real names.
- [ ] Post: correct topic (`roll` vs `standard`); markdown renders; no
secret material.
- [ ] Meme 1080x1080 or flyer 1080x1350; wordmark present; text fully
visible (no clip).
- [ ] Caption/alt English, honest, not SQL-keyword-leading.
- [ ] `GALLERY` updated or post published via admin; retired files not
re-listed.
- [ ] Visually verified (Read/open the image or preview the post).
- [ ] No commit of `.env`, passwords, or private join PII.
---
## 9. Success criteria for the content master
1. Home intro stays hostile, data-backed, and accurately retoor's voice
unless retoor orders a rewrite.
2. Roll accumulates real finishing/AI/craft stories; placeholders shrink.
3. Standard stays taste-and-standards, not tutorial spam.
4. Galleries keep the three-way thesis (classic roast / vibe roast /
elite twice) with consistent visual system and English captions.
5. Featured flyer remains the thesis sheet (elite twice) unless retoor
reorders deliberately.
6. Join queue is handled as private admin work; public site never fakes
community size.
7. No Dutch, no em-dashes, no secrets, no invented people in anything
shipped.
8. New assets are reproducible from the Pillow recipe (or an equal
programmatic successor), not one-off unreproducible binaries without
text sources.
9. Content changes do not require inventing engineering; when gallery
code or settings must change, coordinate with engineering and keep
tests green on their side.
10. Coworker can ship a new meme set or post week using only this
document, admin login (secrets from operator), and the generator
path above.
---
## Quick reference paths
| Item | Path / URL |
|------|------------|
| Git remote | `https://retoor.molodetz.nl/retoor/ad.git` |
| App repo (local) | `/workspace/retoor/molodetz-app` |
| Production | `https://molodetz.nl` |
| Staging | `https://staging.app.molodetz.nl` |
| Test | `https://molodetz-test.app.molodetz.nl` |
| Docs download | `https://molodetz.nl/docs/download.md` |
| OpenAPI | `https://molodetz.nl/swagger` |
| API registry (code) | `molodetz/docs_api/` |
| Download builder | `molodetz/docs_export.py` |
| Intro seed | `molodetz/seed/intro.md` |
| Gallery registry | `molodetz/gallery.py` |
| Media files | `molodetz/static/media/` |
| Seed posts | `molodetz/content.py` `SEED_POSTS` |
| Feature selection | `SELECTION.md` |
| Compliance vs DPP | `COMPLIANCE.md` (engineering; not tone) |
| Engineering lessons (not content) | `DEVELOPMENT_FLOW.md` |
| Meme generator scratch | `/workspace/tmp/molodetz-app-20261005/memes/` |
| Deploy notes | `README.md`, `molohttp_deploy.md` |