Files
retoor 9fb4d65787 Member invites, gallery resync, content and operator docs
- Invite flow: admin issue/revoke on join requests, public single-use
  claim links (hash-only tokens, 7-day expiry, no state reveal), claim
  creates the Member account and marks the request accepted
- Gallery: admin status page plus resync endpoint; sync refreshes
  thumbnails whose content changed; tools/gallery contract and checker
- Docs: public content page, admin-only operator runbook, api.md
  invite/gallery sections, all routes in the live API docs, docs
  reachability gate test
- Screenshots cover the new pages; version 1.0.18
2026-10-06 02:56:08 +02:00

6.1 KiB

Molodetz

Blog roll of the Molodetz community. Quiet, serif, slightly provocative. Posts are markdown in SQLite, written by retoor through an admin area. Flyers and memes are galleries. Join is a real sign-up form ("I'm in"). The whole site is in English; the old Dutch paths (/rol, /standaard, /mensen, /binnen, /voorwaarden) redirect with 301.

Built to the frozen DPP template (/workspace/dpp/dptemplate.md). See SELECTION.md for the module choice and COMPLIANCE.md for the review.

Running

make install          # venv in .venv, pip install -e ".[dev]", Chromium, git hooks
cp .env.example .env  # only when there is no .env yet; chmod 600 .env
make dev              # http://127.0.0.1:8088, reload
make prod             # workers, pinned static version

make install needs a python3 with the venv module (Debian/Ubuntu ship it as python3-venv).

Without make:

.venv/bin/python -m uvicorn molodetz.main:app --host 127.0.0.1 --port 8088

The first start creates the administrator from MOLODETZ_ADMIN_USERNAME / MOLODETZ_ADMIN_EMAIL / MOLODETZ_ADMIN_PASSWORD in .env, seeds the placeholder posts once and syncs the gallery. A one-time migration (system.migrate.english) translates content seeded by older Dutch versions. Log in at /auth/login, admin at /admin.

Testing

make test         # unit + api + e2e (Playwright, headless)
make test-headed  # visible with slow motion
make check-ui     # six static gates + em-dash + json_error order
make preflight    # import, pyflakes, JS syntax, sqlglot lint, check-ui
make coverage
make locust-headless

Tests boot their own server on port 8099; set MOLODETZ_TEST_PORT to a free port when 8099 is taken. Browser tests wait up to 60 seconds per step and retry a stalled page load once via goto_ready in tests/conftest.py.

Docker

docker compose up -d --build                        # staging dev server on http://127.0.0.1:19847, reload on change
docker compose -f docker-compose.test.yml up -d     # public test server on http://127.0.0.1:19849
PROD_SECRET_KEY=... PROD_ADMIN_PASSWORD=... docker compose -f docker-compose.prod.yml up -d --build  # production on :19848
docker compose logs -f                              # follow logs
docker compose down                                 # stop

Each compose file is its own project (molodetz-dev, molodetz-test, molodetz-prod) with its own ./data/<env> file mount, so the three never interfere. Production secrets come only from PROD_SECRET_KEY / PROD_ADMIN_PASSWORD and refuse to boot without them; staging.app.molodetz.nl, molodetz-test.app.molodetz.nl, and molodetz.nl proxy to :19847, :19849, and :19848 (see molohttp_deploy.md).

The container runs the dev server (uvicorn --reload) with ./molodetz mounted, so every saved change is live within seconds. Data lives in the file mount ./data/staging (plain files, no named volumes anywhere). The admin account comes from MOLODETZ_ADMIN_USERNAME / MOLODETZ_ADMIN_EMAIL / MOLODETZ_ADMIN_PASSWORD (default password staging-admin-pass-1, override per shell); an empty password skips admin bootstrap. The compose port is fixed at 127.0.0.1:19847 and is what staging.app.molodetz.nl proxies to, so keep it stable.

make staging-refresh  # copy the holy prod database over staging, then verify

Refresh snapshots ./data/production (online backup API, production keeps running), integrity-checks the copy, stops staging, swaps the staging file mount content, restarts, and verifies health. After a refresh the staging admin password is the production one, so pass it to make screenshots-staging STAGING_ADMIN_PASSWORD=....

Screenshots

make screenshots                                                         # test gallery into ../static/pizdetz/test
make screenshots-staging                                                 # staging gallery into ../static/pizdetz/staging
make screenshots-production SCREENSHOTS_ADMIN_PASSWORD=...               # production gallery into ../static/pizdetz/production

Each run captures 40 full-page desktop shots (public, docs, admin; the test gallery adds a live invite-claim shot) plus a 25-cell responsive matrix (home, roll, flyers, join, docs at 1440x900, 768x1024, 390x844, 360x740, 320x568) with a horizontal-overflow check per cell, then rebuilds the hub page at ../static/pizdetz/index.html from whichever environments exist on disk.

Rules, exactly: test always wipes an isolated ./data/.tmp-shots-<pid>, boots its own server there, and seeds one join request plus one invite; staging and production shoot a live base URL and never seed. Staging logs in with STAGING_ADMIN_PASSWORD (default staging-admin-pass-1, must match the compose admin password); production has no default and fails loudly without SCREENSHOTS_ADMIN_PASSWORD. Any overflow is reported and exits nonzero. Override the output root with SCREENSHOTS_DIR=....

CLI

molodetz --help: roles, API keys, posts list/import (.md and .pdf via pypdf), join requests, backups, prunes, sql-lint, emoji regenerate.

Data

All application data lives under ./data/<env>, enforced in code: molodetz/config.py refuses to start with a MOLODETZ_DATA_DIR outside ./data or a sqlite file outside the data dir.

  • development - local make dev runs (see .env)
  • staging - docker dev server on :19847 (file mount)
  • production - holy production database (file mount, docker prod on :19848)
  • test - screenshot runs, wiped at the start of every run
  • .tmp-pytest-<pid>, .tmp-locust - single test runs, removed afterwards

Each env dir holds molodetz.db (plus WAL files while running) and one subdirectory per kind: uploads (blobs, attachments), backups, keys, locks, seo_reports. The whole tree is git-ignored. Nothing else on the system holds application data; molodetz/static/js/generated/EmojiMap.js is a vendored build artifact the app only rewrites if missing.

Operations

Production serving runs through molohttp on this box. molohttp_deploy.md is the operator guide: access, file owners, the backup specification, site changes, and static publishing.