- 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
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- localmake devruns (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.