From 3a83161b1c7320c4e9c07d528e891923b0752ffa Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Thu, 27 Aug 2026 23:09:42 +0700 Subject: [PATCH] Cinder pass across /admin, the login gate, and the library's a11y floor (#177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One commit (`af07314`), three strands of browser-UI work against one design system. `docs/design-system.md` was updated to match the CSS, not the reverse. ## Library (Reader-facing) Findings came out of a two-axis design review of the library surface; the fixes are the P1/P2 set plus the cheap P3s. - **`.chrome` sticks at `top: 0`.** Search and the tab row were unreachable three screens into a 300-item library — exactly where they earn their keep. Everything above them (`.topbar`, `.keyrow`, `.recent`) still scrolls away on purpose: another 150px of permanent chrome on an 844px phone costs more than re-scrolling for an icon reminder. - **One `:focus-visible` ring** (`2px solid var(--paper)`, offset 2px) on the nine controls that defined none and fell back to the UA blue — a colour tuned for neither branch of this palette. `.searchbar` keeps its `:focus-within` border recolour as a resting cue but no longer stands in for the ring. - **Mono labels lift 10px → 11px** everywhere (nine rules). PRODUCT.md names night reading and glare as the usage scene; 10px small-caps was the one place taste overrode the brief. 11px is now a documented floor. - **A card in flight past 2s says `Saving…` and carries `aria-busy`.** htmx sets neither, so the wait — up to its own 15s timeout, and this app is used on a phone in dead zones — was silent in both the visual and the assistive channel. Deliberately `--mute`, not `--ember`: ember means "new chapter" and nothing else. - **Titles clamp at 3 lines**; `.is-new .title` takes `width: fit-content`, or `-webkit-box` stretches the ember underline past the text it is supposed to be sized to. - `.libswitch a` reaches a real 44px under `(pointer: coarse)` — padding plus an 11px line landed at 43. ## /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 previously assumed rows. - The admin shell picks up the library's chrome: htmx 15s timeout, the shared `#notice` slot, `#sr-announce`, `filter.js`. - `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 numeric guild id — that was never actionable, and the gate still reveals nothing about whether a given guild exists. ## Handlers `maxChapterNum` (9999) now 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 — the poller's `HasNewChapter` comparison, the display string — inherited it. Matches the `max` on the card's chapter input. The API PUT path is deliberately untouched: it carries the userscripts' own scraped numbers, not typed input. ## Verification - `cd backend && go test ./...` green (Docker-backed `pgtest`). `TestChapterOverrideRejectsBadInput` gained `"10000"` and `"1e5"` — both parse fine as `float64`, so they only fail if the bound exists. - Visual: 390×844 dark + light, 1000px and 1440px (`zoom: 1.2`) desktop, against the real templates + real CSS. Measured `chromeTop = 0` at `scrollY 950`, `2px solid rgb(242,236,229)` rings, `content: "Saving…"` at `opacity: 1` after 2.4s, `aria-busy` `true` during / cleared after, `libswitchH = 44` in a `hasTouch` context, no horizontal overflow at either width. - `detect.mjs` on `templates/`: `[]`, exit 0. ## Note on shape The three strands landed as one commit because `admin_series.go` and `style.css` each carry hunks from more than one of them; splitting cleanly would have needed hunk-level surgery. Say the word if you want it split before merge. Reviewed-on: https://gitea.violetcrown.my.id/sulthan/mangaBookmark/pulls/177 Co-authored-by: Sulthan Zaki Co-committed-by: Sulthan Zaki --- .env.example | 6 +- ...ckend-internal-web-templates-admin-html.md | 113 +++++++ ...6-08-27T12-24-48Z__backend-internal-web.md | 135 ++++++++ ...6-08-27T14-36-34Z__backend-internal-web.md | 130 ++++++++ ...backend-internal-web-templates-app-html.md | 116 +++++++ DEPLOY.md | 3 + PRODUCT.md | 6 +- README.md | 1 + backend/internal/web/admin_overview.go | 11 +- backend/internal/web/admin_render_test.go | 59 ++++ backend/internal/web/admin_series.go | 4 +- backend/internal/web/card_render_test.go | 72 +++++ backend/internal/web/discord.go | 18 +- backend/internal/web/static/admin.css | 274 +++++++++++++--- backend/internal/web/static/filter.js | 155 ++++++++- backend/internal/web/static/style.css | 297 +++++++++++++++--- backend/internal/web/templates/admin.html | 12 +- backend/internal/web/templates/app.html | 20 +- backend/internal/web/templates/card.html | 24 +- backend/internal/web/templates/chrome.html | 11 +- backend/internal/web/templates/lanes.html | 21 +- backend/internal/web/templates/list.html | 2 +- backend/internal/web/templates/login.html | 3 +- backend/internal/web/templates/overview.html | 6 +- backend/internal/web/templates/readers.html | 49 ++- .../internal/web/templates/series-detail.html | 61 ++-- .../internal/web/templates/series-list.html | 32 +- backend/internal/web/web.go | 73 ++++- backend/main.go | 1 + backend/web_test.go | 33 +- docker-compose.yml | 6 +- docs/design-system.md | 50 ++- 32 files changed, 1567 insertions(+), 237 deletions(-) create mode 100644 .impeccable/critique/2026-08-27T07-53-05Z__backend-internal-web-templates-admin-html.md create mode 100644 .impeccable/critique/2026-08-27T12-24-48Z__backend-internal-web.md create mode 100644 .impeccable/critique/2026-08-27T14-36-34Z__backend-internal-web.md create mode 100644 .impeccable/critique/2026-08-27T15-38-17Z__backend-internal-web-templates-app-html.md create mode 100644 backend/internal/web/admin_render_test.go create mode 100644 backend/internal/web/card_render_test.go diff --git a/.env.example b/.env.example index 6b9c726..f7058cc 100644 --- a/.env.example +++ b/.env.example @@ -55,13 +55,17 @@ DISCORD_CLIENT_SECRET= # The guild whose membership gates sign-in (Developer Mode -> right-click the # server -> Copy Server ID). DISCORD_GUILD_ID= +# Optional: human-readable name for that guild, shown on the login screen so a +# stranger knows which community owns this library and who to ask for an +# invite. When unset the page falls back to a generic "private community" +# label. +# DISCORD_GUILD_NAME= # Exact callback URL, e.g. https://bookmark.example.com/auth/discord/callback. # Discord matches it verbatim, so it must equal the registered redirect. DISCORD_REDIRECT_URI= # Optional: a role snowflake members must hold on top of guild membership. # Empty (the default) means membership alone suffices. # DISCORD_REQUIRED_ROLE= - # Subdomain Traefik routes to the browser UI (required by the prod override). # Left commented on purpose: an example value here would be a silent # wrong-hostname fallback, and Traefik would publish the UI router on a domain diff --git a/.impeccable/critique/2026-08-27T07-53-05Z__backend-internal-web-templates-admin-html.md b/.impeccable/critique/2026-08-27T07-53-05Z__backend-internal-web-templates-admin-html.md new file mode 100644 index 0000000..4dc5b40 --- /dev/null +++ b/.impeccable/critique/2026-08-27T07-53-05Z__backend-internal-web-templates-admin-html.md @@ -0,0 +1,113 @@ +--- +target: backend/internal/web (admin surface) +total_score: 22 +max_score: 40 +na_heuristics: +p0_count: 1 +p1_count: 2 +timestamp: 2026-08-27T07-53-05Z +slug: backend-internal-web-templates-admin-html +--- +Method: dual-agent (A: AssessDesign · B: AssessEvidence) + +## Design Health Score + +| # | Heuristic | Score | Key Issue | +|---|-----------|-------|-----------| +| 1 | Visibility of System Status | 2 | Domain status is excellent; *interaction* status is absent — no `hx-indicator`/`hx-disabled-elt` in any admin template (only `card.html:52,71,85,108,119` has them). | +| 2 | Match System / Real World | 3 | `Due`/`Checked`/`Gap` (`lanes.html:24`) are unlabeled integers and a raw Go duration (`admin_lanes.go:229-231`); `Never chk` (`overview.html:13`). | +| 3 | User Control and Freedom | 2 | Only Finish has Cancel + reversal (`series-detail.html:51-57`). Clear marks, Revoke, both Removes and both `Set` writes are one-way, and the old chapter survives only as a `placeholder` (`series-detail.html:19`). | +| 4 | Consistency and Standards | 2 | "Needs attention" is `--patina` on the Series list (`admin.css:285-287`) and `--danger` on Lanes/Sites (`admin.css:189-191`, `:409-413`); sibling controls are `` vs ` - @@ -88,8 +95,11 @@ label names the field and the latest sits after it as context. */}}
- {{/* max is a fat-finger guard, not a real ceiling — no series is near it. */}} -
diff --git a/backend/internal/web/templates/chrome.html b/backend/internal/web/templates/chrome.html index 5580bd7..9bd0683 100644 --- a/backend/internal/web/templates/chrome.html +++ b/backend/internal/web/templates/chrome.html @@ -14,8 +14,9 @@ - {{if .Cover}} - {{else}}{{end}} + {{/* Monogram always present under the image; see card.html. */}} + {{if .Cover}}{{end}} + {{if .HasNewChapter}} {{else if .Favorite}}{{end}} @@ -34,11 +35,11 @@
Read Fav - Chapter + Chapter {{if eq .Tab "archived"}} - Restore + Restore {{else}} - Archive + Archive {{end}} Delete
diff --git a/backend/internal/web/templates/lanes.html b/backend/internal/web/templates/lanes.html index 0bf79ac..91eafd3 100644 --- a/backend/internal/web/templates/lanes.html +++ b/backend/internal/web/templates/lanes.html @@ -10,7 +10,7 @@ this template only prints them. */}} {{define "lanes"}}
+ hx-get="/ui/admin/lanes" hx-trigger="every 30s" hx-swap="outerHTML" hx-sync="this:replace">

Poll Lanes

@@ -18,20 +18,23 @@ {{else}}Browser: {{if not .BrowserConfigured}}not configured{{else if .BrowserReachable}}reachable{{else}}unreachable{{end}}{{end}}

+

Healthy idle (paused, browser asleep, nothing eligible) needs no action — attention needs the owner.

{{if .Rows}}
SiteDueCheckedGap - Last passOutcomes · state + Last passOutcomes · status
{{range .Rows}}
{{.Site}} - {{.Due}} - {{.Checked}} - {{.Gap}} + {{/* data-label is the column word for the phone layout, which drops the + thead: three bare numbers in a row say nothing. */}} + {{.Due}} + {{.Checked}} + {{.Gap}} ran {{.Ran}} - {{if .HasChips}}{{range $i, $c := .Chips}}{{if $i}} · {{end}}{{$c.Name}} {{$c.Count}}{{end}}{{else}}none observed{{end}}{{if .StatePhrase}} · {{.StatePhrase}}{{end}} + {{if .HasChips}}{{range $i, $c := .Chips}}{{if $i}} · {{end}}{{$c.Name}} {{$c.Count}}{{end}}{{else}}none observed{{end}}{{if .StatePhrase}} · {{.StatePhrase}}{{end}} {{/* The pause control lives in the one slot the design leaves for it: a running Lane offers the three durations and Pause; a paused Lane offers Resume in the same place. Pause is not destruction — it @@ -39,11 +42,11 @@ confirm row or the danger accent. The form wraps the select so the offered duration travels with the press. */}} {{if .Paused}} -
+
{{else}} -
+ @@ -57,4 +60,4 @@

No data yet — no Lane has recorded a pass.

{{end}}
-{{end}} \ No newline at end of file +{{end}} diff --git a/backend/internal/web/templates/list.html b/backend/internal/web/templates/list.html index b0a9227..5db4b30 100644 --- a/backend/internal/web/templates/list.html +++ b/backend/internal/web/templates/list.html @@ -4,7 +4,7 @@ {{/* The client filter only hides cards, so without this the list area goes blank on a query that matches nothing. filter.js fills in the query and unhides it; it lives inside #list so a tab swap re-creates it. */}} -