# 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` |