-
v0.9.0 Stable
released this
2026-08-30 21:03:40 +02:00 | 3 commits to dev since this releaseThe admin is rebuilt: shared UI primitives, the brand palette and type, an
editorial index in place of the data grid, and song editing at its own URL. The
release also repairs an admin that had been unable to write anything for nine
months, and closes the path traversal that repair would otherwise have made
reachable.Added
- Song editing has its own URL —
/admin/songs/<slug>renders the edit form and a files section, so an edit can be reloaded, shared and reached with the browser Back button. A row'sFileslink deep-links to#files, which keeps uploading one click from the list now that the file dialog is gone - Shared UI primitives —
Modal,Button(variants plus a pending state),Field/TextareaField(label, error and description wiring) and aSongFormextracted from the add and edit dialogs. Each admin surface previously carried its own copy - The design language is enforced by tests —
admin-palettefails the build on any off-palette Tailwind hue, on a colour class with nodark:sibling, and on classes that cannot work in both themes;token-contrastrecomputes WCAG ratios from the real hexes inglobals.css. Two acceptance criteria that a reviewer used to have to remember to grep
Changed
- The admin wears the brand palette and type — Anton uppercase chrome, warm near-monochrome, and red reserved for hover, focus, active state and destructive actions. The Logout button is no longer red
- The songs table is an index list — hairline-divided rows mirroring the public index, with the title as the link to its edit page. A list of songs with per-row actions, not a data grid: nothing sorted or compared across columns, and dropping the table dropped the horizontal scroll wrapper five columns needed
- The dashboard reads top-down — header, then messages, then SONGS with a live count and the Add/Import pair, then the rows, then SETTINGS. The
All Songsheading previously sat immediately above the Settings card with the songs list forty lines below it, so a screen-reader user landing on that heading reached a suggestions toggle and a theme picker - Featured state is written out — each row reads
FeaturedorNot featuredwith a contrast step, rather than a single static word whose only state signal was a red checkbox fill. Colour is now redundant reinforcement, not the sole channel - The edit and file dialogs are retired — both opened over the list; the page behind them is reachable by link
- Heading rules are visible in light theme —
PageHeadingandSectionHeadingdrewborder-neutral-200, which is the light page ground, so their underline rendered at 1.000:1. Identical, not faint
Fixed
- Every admin write route returned 500 —
withValidationconsumed the request body to validate it, and each handler then read it again; a FetchRequestbody is a one-shot stream, so the second read threw and the error surfaced as an internal error. Adding, importing, editing, deleting and featuring a song were all broken, indevsince 2025-11-12. The middleware now validates a clone. Its own JSDoc example had been demonstrating the double read, which is how the pattern reached every route - The file input was unreachable by keyboard — the upload control used
display: none, which removes an element from the tab order, so there was no keyboard path to choosing a file at all (WCAG 2.1.1, level A) - The song row hover band was flush against its text — the band's edges landed on the glyphs, reading as a tight box rather than a row. It now reaches the page gutter while the text keeps its column
Security
- Path traversal in three admin routes —
slugwent into a filesystem path undercontent/guarded only by an existence check.delete-songwould recursively delete any directory given../x,delete-filewould unlink any file through eitherslugorfilename, andedit-songwould overwrite any pre-existinglyrics.md. Post-authentication, and unreachable while the routes were failing, but live the moment they worked. Containment now lives in one place,resolveWithin - The write routes had no authorization of their own — they trusted the middleware alone, while three sibling routes each carried an explicit check. All four now match
- 500 responses leaked absolute server paths — the raw error message was returned to the client, and Node filesystem errors embed paths. Unknown errors now return a constant
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Song editing has its own URL —