Skip to content

M5: Admin (settings, design, URL, delete, export)#14

Merged
simonbc merged 6 commits into
mainfrom
m5-admin
May 15, 2026
Merged

M5: Admin (settings, design, URL, delete, export)#14
simonbc merged 6 commits into
mainfrom
m5-admin

Conversation

@simonbc
Copy link
Copy Markdown
Owner

@simonbc simonbc commented May 15, 2026

Summary

Port the admin section: settings, change site address (public_url), change password (signed-in), design (fonts/colors), soft delete, and a zipped-markdown export. Server-rendered throughout — JS deferred to M9.

Decisions

  • Delete behavior: soft (matches original) — sets deleted=true and clears public_url so the slug frees up.
  • Export format: zipped .md bundle, one file per page (latest revision only).
  • Design fidelity: full 13-field form now (title/subtitle/headings/content × font+color+size, plus header_color, hue, brightness).

Test plan

  • Step 1: DB helpers (change_public_url, is_public_url_available, delete_site, get_design, update_design, get_pages_for_export)
  • Step 2: /admin/settings
  • Step 3: /admin/change-site-address + /admin/url-available
  • Step 4: /admin/change-password (signed-in)
  • Step 5: /admin/design
  • Step 6: /admin/delete + /admin/export

🤖 Generated with Claude Code

simonbc and others added 6 commits May 15, 2026 16:38
Helpers for the admin views in subsequent commits:
- change_public_url / is_public_url_available + RESERVED_PUBLIC_URLS
- delete_site (soft: deleted=True, public_url cleared; pages/revisions
  preserved for a potential recovery path)
- get_design / update_design (allowlists known fields so callers can
  splat form data without worrying about extra keys)
- get_pages_for_export — latest revision per non-deleted page, joined
  with pages.name and revisions.created; used by /admin/export.

First commit of M5. Admin views land in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GET renders the title / subtitle / email / security form pre-populated
from the site row; POST validates and patches via update_site, then
redirects to the site root. Both require auth.gate("admin") — an
anonymous GET redirects to /site/signin with return_to preserved, and
an anonymous POST gets a 401.

public_url is shown read-only on the settings page; changing it
belongs to /admin/change-site-address (Step 3 of this PR) which has
to coordinate with sessions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Change-site-address validates that the new slug is lowercase
alphanumeric + dashes, not on the reserved list, and not already
taken; the JSON probe shares the same checks so the form can call it
live before submit. Empty new value clears public_url so the site
falls back to its secret URL only.

Successful change redirects to /admin/settings on the canonical URL —
either the new subdomain or, when the slug was cleared, the secret-
URL path — because the request's host no longer resolves after the DB
update. Cookie domain is `.jottit.test` (set by Flask from SERVER_NAME),
so the session survives the cross-subdomain hop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GET renders the current-password + new-password form; POST verifies
the current password via argon2, sets the new one, and redirects to
/admin/settings. Wrong current password gets 401, empty new password
gets 400.

Distinct from the token-based recovery flow in /site/change-password:
that path arrives via an email link with `?d=token` and no current
password; this one is for a user who's already signed in.

Sessions are keyed by site_id, not password, so the user stays signed
in across the change (unlike the 2007 original whose session digest
bound to the password and had to be re-issued).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full-fidelity port of the 13 design fields: four fonts, three colors,
four sizes, plus hue and brightness. GET prefills from the design row;
POST validates each field by type (hex colors, numeric sizes in
[25, 500], hue in [0, 360], brightness in [0, 300]) and rejects fonts
containing HTML metacharacters as a soft XSS guard.

Empty form values leave the existing column untouched (update_design's
sentinel is `None`, which the validator-then-coercer pipeline collapses
to "not in the dict at all"). After a successful save the user is
redirected back to /admin/design so they see the persisted values.

JS-driven color picker / live preview waits for M9; the form is plain
inputs for now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Delete is soft: marks sites.deleted=True, clears public_url so the
slug frees up, signs the visitor out of just that site, and redirects
to the apex homepage (the site no longer resolves at its old URL).
Pages and revisions stay on disk in case a future admin path wants to
restore.

site_resolver now treats deleted=True sites as unresolved — they
disappear from both subdomain and secret-URL routes, same as if the
slug had never existed. That's the change that makes "delete actually
hides the site" end-to-end.

Export builds a zip in memory with one .md per non-deleted page
(latest revision only) and streams it as an attachment. Filename
inside the zip swaps slashes/backslashes for underscores so a page
name like `../oops` can't escape the flat namespace.

Closes out M5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@simonbc simonbc merged commit 0025284 into main May 15, 2026
1 check passed
@simonbc simonbc deleted the m5-admin branch May 15, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant