Raw error red fails AA as text even with no tint behind it: in dark mode
it measures 3.63:1 on obsidian, 3.23 on iron and 2.64 on slate, against
4.5. error-fg (the house formula, 50% toward parchment) measures 5.30 at
worst across both modes.
- All 19 text-error uses become text-error-fg: the "Couldn't load"
messages on the admin pages, the integrations form errors, the flag
popover, and the error toast's text. The toast keeps its error border,
since a border is a graphic with a 3:1 floor.
- check-tint-contrast flags raw error text anywhere (text-error,
class:text-error, color: var(--fs-error)) and leaves borders and
outlines alone. Run against the files before the swap, it finds all 19.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hue painted as text on a color-mix tint of itself sits close to the
surface under it. On Minstrel's surfaces the raw accent on its 15% tint
measures 1.97:1 at worst (dark mode, hover surface), against AA's 4.5.
- tokens.json gains colors.fg: the five FabledSword -fg formulas (accent
45%, success 45%, warning, error and info 50%), each mixed toward
parchment so one declaration serves both modes. Success is Minstrel's
moss. tokens-to-css emits them in :root.
- Tailwind exposes them as text-accent-fg, text-warning-fg, text-error-fg
and text-info-fg.
- 23 sites swapped: 14 Tailwind class strings (PlayerBar and the admin
count pills) and 9 CSS rules (StatusPill's four tones and five accent
chips). Worst case after: accent-fg 5.03, error-fg 4.75, warning-fg
4.92, success-fg 4.85.
- scripts/check-tint-contrast.js finds the pair in either spelling. Its
test scans src in the web Vitest lane and fails on any new site, with
fixture cases showing it can fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Admin quarantine, admin playback-errors and library/hidden each carried
a byte-identical private copy of the same coarse "3d ago / 5h ago /
12m ago / just now" formatter. Writing the missing-files surface would
have made it four, so extract it instead.
They are one concept, not three that happen to look alike: each shows
the age of something an operator is deciding about, and they have to
agree -- a row reading "2d ago" on one screen and "2 days" on another
makes the reader wonder whether the two mean different things.
Three near neighbours are deliberately NOT folded in, because they are
different intents rather than drifted copies:
- HistoryRow shows a weekday and clock time under a week ("Tue 21:40"):
for listening history, WHEN you played something beats how long ago.
- ActiveSessions.when() writes prose ("1 hour ago", "yesterday") and
falls back to a locale date past 30 days -- a security surface where
the longer form reads better.
- PlaylistCard.refreshedLabel() is day-boundary aware and prefixed
("Refreshed today"), and already carries a comment saying it is
deliberately not the m/h-ago style.
Merging any of those would mean forcing one caller's wording onto
another, which is the wrong-abstraction failure, so they stay put.
Tests pin the boundaries the copies never covered: each unit step, that
only the largest whole unit is reported (25h is "1d ago", never
"1d 1h ago"), and that a future timestamp from a skewed client clock
degrades to "just now" instead of rendering a negative age.
CI vitest run on 3f8a2c5 surfaced 17 failures across 5 test files; this
commit addresses 15 that are this batch's responsibility.
1. RowActionsMenu now accepts ariaLabel on RowAction. Defaults to label.
Admin pages (requests/quarantine/users) pass per-row aria-labels
matching the pre-batch buttons ("Approve Geogaddi", "Resolve Roygbiv",
"Make alice admin", etc.) so screen readers + tests find them.
2. PlayerBar.test.ts — anchored regex /^(play|pause)$/i so the new
"Player options" overflow ⋮ doesn't also match /play|pause/i.
3. MobileNavDrawer.test.ts — added vi.mock for $app/state, $app/navigation,
and $lib/auth/store.svelte (mirrors Shell.test.ts pattern). Without
these, SvelteKit's notifiable_store helper isn't bootstrapped in
vitest and the suite fails to load.
The 2 remaining vitest failures are in /admin/integrations Save flow
(putLidarrConfig spy not called). Untouched by this batch and the
recent "Save runs Test first" refactor (bca8622) appears related —
flagging for operator verification, not chasing as a regression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Svelte 5 requires {@const} to be a direct child of certain blocks
({#snippet}, {#if}, {#each}, etc.). Placing them inside <li>
bodies broke the build with const_tag_invalid_placement. Moved
the RowAction const declarations up so they sit between {#each}
and the <li> opener; the each-binding (u/r) is in scope for the
entire each block body, so the consts behave identically.
Caught by local docker build on dev:
src/routes/admin/users/+page.svelte:259:12
https://svelte.dev/e/const_tag_invalid_placement
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Requests: primary Approve, secondary [Override, Reject].
Quarantine: primary Resolve, secondary [Play, Delete file]; the
Delete-via-Lidarr conditional stays inline above md (preserves the
disabled-with-tooltip semantics) and hides below md to avoid
crowding — the operator can still trigger it from the desktop view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add browser tab titles to all 18 static page routes using the pageTitle()
helper from $lib/branding. Titles follow the Minstrel · Section format.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three admin pages had divergent inline error-code → user-copy switch
tables (with wording drift, including a stale "Settings → Integrations"
that should have been Admin). Consolidate into web/src/lib/styles/error-copy.json,
expose via web/src/lib/api/error-copy.ts (ERROR_COPY + copyForCode),
and refactor the three pages to import the helper.
Fixes the three-way drift; the only user-visible behavior change is
the quarantine page now correctly says "Admin → Integrations".
Sets up the JSON for the M7 Flutter client (#356) to consume the same
table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>