Skip to content

Admin Panel

pH7x Systems edited this page Jul 21, 2026 · 46 revisions

Admin Panel

apps/admin (package cms-admin): one FastAPI process serving the API and the server-rendered UI — Jinja templates on AdminLTE 4 (MIT, vendored verbatim with its license; Bootstrap 5 bundled), implemented 1:1 as its reference pages do — Source Sans 3 (OFL), Bootstrap Icons (MIT) and the theme's own behavior scripts vendored locally: collapsible sidebar, fullscreen toggle, light/dark/auto color mode (WCAG-gated in both palettes in CI), the canonical user menu. No CDN, no inline scripts, no build step. It drives cms-core, cms-validation and cms-build through their public APIs and never bypasses them.

Try it without installing anything: the demo site ships a read-only static snapshot at https://sardine.ph7x.com/admin/ — the real admin's pages, captured with demo content; nothing there saves, by construction.

Run it

pip install -e apps/admin
cms admin create-user editor-in-chief --role admin   # prompts for a password
SARDINE_STORAGE_URL="sqlite:///content.sqlite3" \
SARDINE_ADMIN_COOKIE_SECURE=0 \
  uvicorn --factory cms_admin.app:create_app

Sign in at /login. There are no default credentials — the first account always comes from the CLI. Passwords contain 12–1024 characters. Replacing an account through the CLI revokes all of its existing sessions.

Configuration (environment only — no config files with secrets)

Variable Default Meaning
SARDINE_STORAGE_URL sqlite:///content.db Storage URL (create_storage)
SARDINE_ADMIN_SESSION_HOURS 12 Session lifetime
SARDINE_ADMIN_COOKIE_SECURE 1 Set 0 only for plain-http local development
SARDINE_MEDIA_DIR media Media upload directory (the project's media/)
SARDINE_ADMIN_UPLOAD_MAX_MB 10 Upload size limit in MB
SARDINE_ADMIN_UPLOAD_MAX_PIXELS 40000000 Maximum decoded image pixel count
SARDINE_PROJECT_DIR . Project directory holding sardine.toml (panel builds)
SARDINE_ADMIN_PUBLISH_GATE 1 Set 0 to allow publishing despite validation errors

Accounts and security

  • Roles (least-privilege ladder): editor < reviewer < publisher < admin, enforced server-side.
  • Passwords: argon2id hashes; sessions server-side with only the token digest stored; production cookies use the __Host- prefix plus HttpOnly, Secure and SameSite=Strict.
  • CSRF: constant-time token comparison on state-changing requests and a short-lived double-submit login token. Unknown users receive equivalent Argon2 work; bounded client/account rate limits and password concurrency resist enumeration and resource exhaustion. Multi-process deployments additionally rate-limit /login at the shared ingress.
  • Preview artifacts and uploaded media require a valid session; sensitive responses use HSTS and Cache-Control: no-store.
  • Accounts live in the storage database (shared migrations) and are never exported.

What works today (Milestone 3, phases 1–8)

  • Dashboard: content by workflow status, the translation coverage matrix (missing/outdated/complete per language), a live validation report with the publish-gate verdict.
  • Articles: list with a coverage summary per entry, create, source-language editor (slug, category, tags, cover) with a preview rendered by the builder's own Markdown renderer (raw HTML disabled), and a side-by-side translation editor per language — the source content read-only next to the translation form. The source and target sets come from the project's sardine.toml.
  • Needs attention: the dashboard opens with work, not totals — review queue, pending translations, scheduled changes in the next 7 days, stale drafts; every card links to where the work gets done (#135).
  • Activity (admin-only): the audit trail — sign-ins and failures, transitions, trash/purge, media, user/role/2FA changes and builds; append-only, survives subject deletion, filterable, configurable retention (#134).
  • Scheduled unpublish: unpublish_at beside the publish moment — after it passes, the next build drops the entry (deterministic window end; contradictory windows refused) (#133).
  • Calendar: the month in UTC as the panel schedules — published entries on their day, scheduled on their firing day, drag-to-reschedule keeping the time of day (#132).
  • Translations: the translator's worklist — every missing/outdated entry-language pair for the configured set (pack tags included), filtered by language/state/type, linking to the side-by-side editors; the content lists gain a "missing «tag»" filter (#131).
  • Bulk actions: checkbox selection on the content lists (no JavaScript needed); transitions, trash, category assignment and unreferenced-media deletion apply per entry with the exact single-action rules, and the result page reports every outcome — refusals with their reason (#130).
  • Search: a navbar box on every screen; grouped results over articles, pages, sections and media in every language, each hit linking straight to its editor; trashed entries never match. Storage-level queries (ADR-0038) measured well under the 300 ms budget at 10k entries.
  • Automated deployment (filesystem provider): publish/unpublish end on the public site — immutable releases, atomic activation, health check with auto-rollback, manual rollback without rebuild, scheduled-window watcher, all audited (#156).
  • Publishing and deployment: the build form asks "where will the site live?" with a guided card per target (static host / Azure Static Web Apps / nginx); the choice persists in sardine.toml and cms export defaults to it; a successful build says where the files are and the one action that puts them live (#128).
  • First-run setup: an instance with no accounts sets itself up in the browser — first admin account, site identity (languages from the registered packs, theme; sardine.toml written only when absent), optional example content, straight into a first-steps checklist. The wizard disappears once any account exists (#128).
  • Pages and sections: metadata editor with an optional long-form Markdown body; sections add from a visual block gallery (one card per kind, auto-derived keys), duplicate, hide/show (hidden sections keep content but leave builds and never block parity), delete with undo, and reorder by drag-and-drop with up/down buttons as the no-JS path (#127). Each section opens a three-part editor (ADR-0037): flat fields with kind-aware suggestions, declared Markdown fields using the article-body widget, and the unbounded items table (blank rows add, a cleared row removes). Per-section side-by-side translation mirrors all three with the source read-only beside each input.

Editing a source marks its translations outdated automatically (checksum model). Statuses are displayed but transitions arrive with the workflow phase.

  • Media library: raster uploads validated server-side (MIME sniffed from bytes, bounded read, byte/dimension/pixel limits); active SVG is rejected and exclusive writes never replace paths. EN alt text is mandatory and deletion remains reference-checked.

  • Menu manager: explicit navigation with per-language labels, ordering and external links — defined items replace the automatic menu, an empty list keeps it.

  • Editorial notes & quick actions: a team-only note trail on every entry (never published; author-or-admin removal) and a per-row actions dropdown on the lists — workflow moves and trash without opening the editor.

  • Design preview + autosave: each editor frames the entry rendered by the real builder and theme. Valid edits autosave after a short debounce and refresh only that entry; invalid intermediate forms pause without overwriting content, while explicit saves retain revision semantics.

  • Featured & authorship: a Featured flag (featured articles lead the home highlight; listings stay recency) and an editorial byline rendered by the themes.

  • Media filters: server-side search (id, path, type, alt) and quick views — images only, missing translated alt — with a shown-of-total counter.

  • Media collections and usage (#136): optional collection (folder) per asset, set at upload or edited later, with a collection filter in the library; per-asset usage counts in the list and full reference lists on the asset page; uploads carrying bytes identical to an existing asset are refused, naming that asset (SHA-256).

  • Users screen: admins create accounts (role + panel language), change roles and delete from the panel — you cannot delete yourself, and the last admin can neither be deleted nor demoted.

  • Duplicate & per-entry preview: one-click duplicate-as-draft (collision-safe ids, workflow reset) and a direct jump from each editor to the entry's URL inside the preview build.

  • Trash: reversible deletion — trashed articles and pages disappear from builds, validation, export and lists; restore is exact, and "Delete forever" (admin-only, trash-only) is the panel's single permanent removal.

  • Revisions: every save keeps a snapshot (newest 20 per entity); editors show the history with a per-revision diff against the current content and one-click restore — the restore itself is recorded, so it is always undoable.

  • Scheduled publishing: articles and pages take an optional "Publish at (UTC)" moment — published content stays out of every build until a build runs after it (the build is the clock on a static host; a CI cron recipe ships in the admin guide). Scheduling never bypasses validation or the publish gate.

  • Markdown editor: article bodies get a real editing surface (EasyMDE, MIT, vendored — no CDN) with a localized formatting toolbar; the builder's server-rendered preview remains the single truth for what publishes, and the plain textarea still works without JavaScript.

  • Panel language (i18n): real gettext localization, with catalogs carried by language packs (ADR-0034) — PT-PT, ES, FR and DE ship inside cms-core's packs alongside the English source, and an extension pack carrying a catalog becomes a panel language the moment it is activated, listed by its native name and mirrored (dir="rtl") when the pack declares it. Each editor picks a language in the user menu (or cms admin create-user --language), with the browser's language as fallback; an anti-drift test keeps every bundled catalog complete.

  • Workflow and publishing: role-gated transitions (draft → review → published → archived, including one-click unpublish straight back to draft; publishing needs the publisher rung), the publish gate blocking on the entity's validation errors, and the Publishing panel — preview builds served under /preview/, build & export with target extras written to the project output, every run recorded on the dashboard.

  • Validation report (dashboard + Publishing panel): the gate never hides its work — a callout states the verdict and the validated scope (articles, pages, media, languages), a table lists every rule that ran with its outcome (passing rules included), and each issue links straight to the edit screen of the content that caused it. cms validate prints the same per-rule outcomes in the terminal. The seeded demo keeps one article in review with a missing DE translation, so the report always shows a live warning in action.

  • Hardening: enforced CSP/HSTS/no-store headers, authenticated preview/media, hostile-input regressions, a required dependency/static security gate, managed CodeQL and the axe accessibility gate. The full admin guide lives in docs/ADMIN_GUIDE.md, anti-drift-checked.

Milestone 3 is complete: the full editorial cycle — create, translate, review, publish, build, export — works from the browser, with every security and accessibility gate green in CI. (role-gated transitions, publish gate, builds/exports from the panel), and the hardening pass (CSP, security suites, axe over admin pages). The phased plan lives in docs/PLAN.md.

The demo snapshot

python -m cms_admin.demo_export --storage content.sqlite3 --out _site/admin boots the real admin on a throwaway copy of the content, signs in as a generated demo user and captures every editorial page as inert static HTML (links prefixed, CSRF stripped, forms neutralized, read-only banner). The demo deploy runs it automatically.

Password reset and email (ADR-0032)

Outbound email is off by default and transport-pluggable: SARDINE_MAIL_TRANSPORT names the transport (smtp is the bundled baseline — SARDINE_SMTP_URL + SARDINE_MAIL_FROM; any other name resolves to an activated extension's mail transport, where passwordless provider APIs live). With email on and an address on the account (Users screen or cms admin create-user --email), the login page offers a password reset that never reveals whether an account exists: hashed single-use tokens, 30-minute expiry, password policy enforced, every session revoked on completion. Messages are plain text in the recipient's panel language.

With email on, two editorial events notify by mail (ADR-0032): a transition into review mails every reviewer-and-above account with an address (except the actor), and publishing mails the entry's most recent editing author. Delivery is fire-and-forget; a transport failure never becomes an editorial error.

Two-factor authentication (ADR-0035)

Self-service TOTP from the user menu: enter the shown key in any authenticator app and confirm with a valid code. Sign-in then requires the current code; codes are single-use, wrong codes spend the same rate-limit budget as wrong passwords, and disabling requires a valid code. Enforcement: SARDINE_ADMIN_REQUIRE_2FA sets the minimum role at/above which two-factor is mandatory — covered accounts sign in but are corralled to the enrolment page until they confirm a code, and disabling is refused while the policy applies. Recovery: cms admin create-user --force (replaces the account, clears the second factor, revokes sessions). Password reset by email deliberately does not clear it.

On-publish webhooks (ADR-0036)

SARDINE_WEBHOOK_URL + SARDINE_WEBHOOK_SECRET: transitions that change the public site (into and out of published) POST a minimal signed JSON doorbell to the receiver — verify X-Sardine-Signature (HMAC-SHA256 of the body), answer 2xx quickly, rebuild asynchronously. Three retries with backoff, then the failure is recorded; the scheduled-build recipe stays the safety net. HTTPS required (loopback exempt).

Translations in lists

Content lists show one constant-width coverage cell per entry (3/4 · 1 missing) linking to the editor — never a badge or column per language: language sets are unbounded (ADR-0034), so no admin surface may grow horizontally with them. Per-language detail lives in the entry's editor.

Clone this wiki locally