Cinder pass across /admin, the login gate, and the library's a11y floor

Uncommitted work from three design runs on this branch, against one design
system: docs/design-system.md is updated to match the CSS, not the reverse.

Library (Reader-facing):
- .chrome sticks at top: 0. Search and the tab row were unreachable three
  screens into a 300-item library, which is exactly where they earn their
  keep; everything above them still scrolls away on purpose.
- One :focus-visible ring (2px --paper) on the nine controls that defined
  none and fell back to the UA blue. .searchbar keeps its border recolour as
  a resting cue but no longer stands in for a ring.
- Mono labels lift 10px -> 11px everywhere. The brief names night reading and
  glare as the usage scene; 10px small-caps was where taste overrode it.
- A card in flight past 2s says "Saving..." and carries aria-busy. htmx sets
  neither, so the wait up to its 15s timeout was silent in both channels.
- Titles clamp at 3 lines; .is-new .title takes width: fit-content, or
  -webkit-box stretches the ember underline past the text it sizes to.

/admin:
- Overview routes into Lanes when a lane is unhealthy, prefixes each figure
  with its column word on the phone layout that drops the thead, labels state
  cells for a screen reader, and has an empty state where the sites table
  assumed rows.
- The admin shell picks up the library's chrome: htmx 15s timeout, the shared
  #notice slot, #sr-announce, filter.js. admin.css follows the same pass.
- admin_render_test.go and card_render_test.go render the templates directly,
  so markup regressions in either surface fail without a browser.

Login:
- DISCORD_GUILD_NAME (optional) names the community on the login screen and
  in the refusal message, so a stranger knows which Discord to ask for an
  invite. Unset degrades to a generic label; neither form names the guild id.

Handlers:
- maxChapterNum (9999) bounds both typed-chapter paths. uiChapter and
  adminSeriesCorrectLatest each parsed a float64 with no ceiling, so a
  hand-rolled POST stored 1e308 and every later reader of that row inherited
  it. Matches the max on the card's chapter input.

go test ./... green.
This commit is contained in:
2026-08-27 23:05:40 +07:00
parent cddd16bcdc
commit af07314bb6
32 changed files with 1567 additions and 237 deletions
+3 -3
View File
@@ -8,7 +8,7 @@ web
## Users
Members of one private Discord guild, each with their own library. Accounts exist and are created by signing in — there is no signup form, no invite code and no approval step: any member of the configured guild becomes a Reader on their first Discord login. The person running the deployment is the owner, seeded at startup, and the only Reader with an administrative capability (revoking another Reader's sessions).
Members of one private Discord guild, each with their own library. Accounts exist and are created by signing in — there is no signup form, no invite code and no approval step: any member of the configured guild becomes a Reader on their first Discord login. The person running the deployment is the owner, seeded at startup, and the only Reader with an administrative capability (`/admin` — Overview, Lanes, Readers, Series).
Reading happens on **asurascans.com**, **demonicscans.org**, **comix.to** and **kagane.to** for manga and **novelfull.com** and **lightnovelworld.net** for novels, primarily via Bromite on mobile, with checks and corrections from a desktop browser. The web UI is the cross-device view into progress the userscripts capture while reading.
@@ -34,7 +34,7 @@ Not a public reading tracker or social app — a private, self-hosted sync layer
- "Continue reading" horizontal strip for series with an unread chapter.
- A Reader with no bookmarks at all sees a deliberate empty library offering both userscript install links, not an error and not a blank page.
- Isolation is the load-bearing invariant: two Readers cannot see or change each other's bookmarks. A series both track is one shared row polled once, with independent progress on each side.
- The owner can revoke a specific Reader's sessions; nothing else in the UI differs by Reader.
- The owner alone can reach `/admin` (Overview, Lanes, Readers, Series) for library/lane/series maintenance and Reader session/Sighting controls; otherwise nothing else in the UI differs by Reader.
- htmx-driven partial updates, no client-side framework or build step — templates are Go `html/template`, `go:embed`-ed.
- Mobile-first is a hard functional constraint (primary device is a phone), not just a starting breakpoint.
@@ -54,7 +54,7 @@ Not a public reading tracker or social app — a private, self-hosted sync layer
- Mobile is the primary target; desktop is an enhancement, not the design center.
- Progress data integrity over visual flourish: `updated_at`/list-ordering behavior is a correctness constraint the UI must respect, not decorate over.
- A leak between Readers fails silently and looks like working software — isolation is asserted from both directions, never inferred from counting one Reader's rows.
- No roles, no org chrome: the owner's Readers panel is one list with one button (revoke someone's sessions), not an admin console, and otherwise every Reader's view is the same.
- No roles, no org chrome beyond the owner gate: `/admin` is the one owner-only console (Overview, Lanes, Readers, Series) for library-wide hygiene, lane health/pauses and series/Reader maintenance — Readers offers revoke sessions and clear Sighting marks, both confirm-gated — and otherwise every Reader's view is the same.
- Prefer native platform affordances (system dark/light, native touch targets) over custom widgetry — this is a lean self-hosted tool, not a product to demo.
## Accessibility & Inclusion