Media management: Library page and editor picker #194

Merged
rosa merged 6 commits from media/management into main 2026-08-12 22:21:15 +00:00
Owner

Implements the media-management surface from the spec in #188 — the Library page and the post editor's picker. One PR for the whole effort: the remaining slices land as further commits on this branch.

Slices

  • Filename capture (#189) — already on main (#193).
  • Domain ops (#190) — this branch so far.
  • Library page (#191) — routes, nav + dashboard links, grid, upload, delete confirm flow.
  • Editor picker (#192) — form fields, precedence extension, shared grid partial, JS asset, both editors.

Landed here

The domain vocabulary the web surface will consume:

  • list_media_for — a user's whole Library, newest-first, ordered by the repository rather than by callers.
  • resolve_owned_photo — turns a picked Media id into an author-owned PhotoRef. Foreign and absent ids are alike None, so the outcome can't be used to probe for another user's ids. No status gate, matching what inline upload already allows.
  • list_posts_referencing — the Posts a Media appears in, any status, newest-first; owner-scoped, so a Media the asker doesn't own reveals nothing.
  • delete_media — owner-scoped, with missing and foreign ids collapsing to one not-found. A still-Processing Media is refused, its integrity being the Reconciler's until it settles; Ready and Failed both delete, detaching every referencing Post's Photo — orphaned alt text included — and dropping the rendition. Disk returns on the existing reclamation tick.
  • resolve_photo (Micropub) keeps its URL parse and delegates existence + ownership to resolve_owned_photo, putting that decision in one place. Behavior unchanged, existing tests untouched.

The three new port methods are implemented in both the SQLite and in-memory repositories, the mock mimicking the detach; deleting an unknown id is an idempotent no-op at the repository layer, so a concurrent-delete race can't manufacture an error. Four both_adapters! conformance behaviours cover the new ops against both implementations, detach included.

CONTEXT.md's Library entry records warn-and-allow deletion and the Processing refusal. No new ADR, per #188 §3.

mise run format, mise run ci, and mise run clippy pass.

Closes #190

Implements the media-management surface from the spec in #188 — the Library page and the post editor's picker. One PR for the whole effort: the remaining slices land as further commits on this branch. ## Slices - [x] **Filename capture** (#189) — already on `main` (#193). - [x] **Domain ops** (#190) — this branch so far. - [x] **Library page** (#191) — routes, nav + dashboard links, grid, upload, delete confirm flow. - [x] **Editor picker** (#192) — form fields, precedence extension, shared grid partial, JS asset, both editors. ## Landed here The domain vocabulary the web surface will consume: - `list_media_for` — a user's whole Library, newest-first, ordered by the repository rather than by callers. - `resolve_owned_photo` — turns a picked Media id into an author-owned `PhotoRef`. Foreign and absent ids are alike `None`, so the outcome can't be used to probe for another user's ids. No status gate, matching what inline upload already allows. - `list_posts_referencing` — the Posts a Media appears in, any status, newest-first; owner-scoped, so a Media the asker doesn't own reveals nothing. - `delete_media` — owner-scoped, with missing and foreign ids collapsing to one not-found. A still-Processing Media is refused, its integrity being the Reconciler's until it settles; Ready and Failed both delete, detaching every referencing Post's Photo — orphaned alt text included — and dropping the rendition. Disk returns on the existing reclamation tick. - `resolve_photo` (Micropub) keeps its URL parse and delegates existence + ownership to `resolve_owned_photo`, putting that decision in one place. Behavior unchanged, existing tests untouched. The three new port methods are implemented in both the SQLite and in-memory repositories, the mock mimicking the detach; deleting an unknown id is an idempotent no-op at the repository layer, so a concurrent-delete race can't manufacture an error. Four `both_adapters!` conformance behaviours cover the new ops against both implementations, detach included. CONTEXT.md's Library entry records warn-and-allow deletion and the Processing refusal. No new ADR, per #188 §3. `mise run format`, `mise run ci`, and `mise run clippy` pass. Closes #190
Listing a user's Media, resolving a picked id into an author-owned Photo,
listing the Posts an image appears in, and deleting one with detach.

Deleting a Media clears the alt text alongside the reference, so no Post
is left describing an image it no longer has. Micropub's photo resolution
keeps its URL parse and delegates existence and ownership, putting that
decision in one place.

Closes #190
refactor: listing a Media's referencing Posts is owner-scoped
All checks were successful
ci/woodpecker/push/clippy Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
47f45cb79b
Every other Library op answers only for the asker's own Media; this one
answered for anyone's, leaving the caller to guard it. A Media the asker
does not own now reveals nothing, matching what an absent one reveals.
feat: the Library browses, uploads, and deletes a user's own Media
Some checks failed
ci/woodpecker/push/clippy Pipeline was canceled
ci/woodpecker/push/test Pipeline was canceled
813cf17132
The signed-in owner gets a grid of every Media they have uploaded, an
upload form that is always present, and a warn-and-allow delete
confirmation listing the Posts whose Photo is about to be detached.

The upload form submits to POST /media/upload rather than POST /media:
that path is the Micropub media endpoint, which answers to a bearer
token rather than a session.

Owner scoping now has one home in the domain — find_media_for answers
for a Media someone else owns exactly as for one that was never minted,
and every owner-facing op reaches its subject through it. Deleting takes
the Media itself, so an empty reference listing can no longer stand in
for a refusal.

Closes #191
feat: the post editor attaches a Media picked from the Library
Some checks failed
ci/woodpecker/push/test Pipeline was canceled
ci/woodpecker/push/clippy Pipeline was canceled
e95f511609
Beneath the file input both editors fold away a grid of the author's own
Media. Picking one attaches it as the Post's Photo instead of uploading a
new file: each tile is a label around a radio in the photo_media_id group,
so a pick submits with no script at all, and /js/media-picker.js adds the
preview, the clear control, and the mutual exclusion with the file input on
top of markup that already works without it.

Processing tiles are shown unselectable behind a badge; Failed ones are left
out, having nothing to attach. A submitted Photo now resolves
remove > upload > pick > keep, and a pick naming an unknown or foreign
Media — which only a forged form can send — is refused without saying which
of the two it was.

Closes #192
refactor: the media surfaces name each concept in one place
All checks were successful
ci/woodpecker/push/clippy Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
4f3984f5a7
Review follow-up on the Library and picker work.

The status vocabulary and the pixel-extent string were being rebuilt in the
view; both now answer from the domain model. The Library's user-facing copy
says "Library", the name the glossary gives that surface, rather than naming
the entity it holds. A picked Media travels as a MediaId from the form edge
inward instead of a String compared by ==, and the id-to-404 parse behind the
media handlers is written once. The two grids share their thumbnail markup and
its stylesheet rule, the post editor's four library fetches collapse to one,
and the PNG fixture the server tests upload lives beside the other fixtures.

The repository's delete detaches a Post's alt text as well as its Media, an
invariant the FK's SET NULL was masking: the conformance suite stayed green
with the UPDATE removed. A SQLite-level test now reads the raw columns past
the Post mapping, since neither the port nor the memory adapter exposes an alt
independent of the Photo that carries it.
fix: a post editor submission survives the fields nobody filled in
All checks were successful
ci/woodpecker/push/clippy Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
c49743e6ac
A browser submits every control it renders, so an untouched publication
date arrived as an empty string and failed the YYYY-MM-DD shape check.
Reading blank as absence puts the validator back on values someone typed.

The rejection that followed then re-rendered an empty editor, because the
new-post textarea carried its text in a `value` attribute — which HTML
ignores there — so the round trip read as a page refresh that lost the
work rather than as a form with errors to correct.

The Library is named "Media library" wherever it is reached, and only
from the dashboard: "Library" alone said nothing about what it holds,
and the nav had no room for a fourth destination.

Closes #188
rosa changed title from WIP: Media management: Library page and editor picker to Media management: Library page and editor picker 2026-08-12 22:04:03 +00:00
rosa merged commit be8cb4d3aa into main 2026-08-12 22:21:15 +00:00
rosa deleted branch media/management 2026-08-12 22:21:15 +00:00
Sign in to join this conversation.
No description provided.