Files
mangaBookmark/.impeccable/critique/2026-08-27T14-36-34Z__backend-internal-web.md
T
sulthan 3a83161b1c Cinder pass across /admin, the login gate, and the library's a11y floor (#177)
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: #177
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
2026-08-27 23:09:42 +07:00

19 KiB
Raw Blame History

target, total_score, max_score, na_heuristics, p0_count, p1_count, timestamp, slug
target total_score max_score na_heuristics p0_count p1_count timestamp slug
backend/internal/web 26 40 0 2 2026-08-27T14-36-34Z backend-internal-web

Method: dual-agent (A: AssessDesign-2 [designer] · B: AssessDetector [scout])

Note: Assessment A completed its analysis but hung in a yield loop after writing its report; recovered from its on-disk artifact and cancelled. Two of its claims failed parent verification and are corrected below.

Design Health Score

Mode: Operate (all 10 heuristics apply; none n/a).

# Heuristic Score Key Issue
1 Visibility of System Status 3 Busy hairline is grey, not ember (style.css:865-877), chrome refreshed out-of-band on every mutation (web.go:397) — but the horizontal Recent strip hides its own overflow (style.css:471-476)
2 Match System / Real World 3 Reader copy is native ("Chapter you're on" card.html:96); admin leaks ops jargon — "Sighting … deferral blocked" readers.html:10-12, "Outcomes · status" lanes.html:26
3 User Control and Freedom 2 Esc closes panels and returns focus (filter.js:163-170), Restore is one-tap undo (card.html:75) — but Remove and Archive have no post-action undo, only a pre-action gate
4 Consistency and Standards 3 Action order play∣fav∣chapter∣lifecycle is invariant (card.html:48-86); admin is a second system — patina accent (admin.css:10-14) and --measure-wide: 1080px (admin.css:2) vs --measure: 760px (style.css:124)
5 Error Prevention 3 max=9999 + step=any (card.html:99-102), no-op chapter save skipped (web.go:547-550), every move out of the list confirm-gated (card.html:111,123); no inline validation on the chapter field
6 Recognition Rather Than Recall 3 Permanent keyrow (chrome.html:35-45) decodes the icon strip — but it is hidden while filtering (filter.js:22-24), exactly when cards are being scanned
7 Flexibility and Efficiency 2 Instant client-side filter (filter.js:1-13), bookmarkable tabs (app.html:61); no keyboard shortcut, no bulk archive, no swipe — curation is per-card taps
8 Aesthetic and Minimalist Design 3 Hairlines not cards, single column, Instrument Serif titles (style.css:590); phone chrome stacks ~220px (search style.css:347 + tabs + keyrow style.css:420 + strip style.css:453) before the first card
9 Error Recovery 3 Per-slot inline errors with verbatim server reason (filter.js:200-224,260-263), 401 → "Log in again" (filter.js:273), no self-destruct timer (filter.js:241-245); tab-switch failures land in the easy-to-miss global #notice (style.css:830)
10 Help and Documentation 1 Help is only empty states (list.html:20-26, setup.html:9-17); a guild outsider gets one sentence (login.html:28) and an opaque refusal (discord.go:185) with no guild name and no next step
Total 26/40 Competent, with one weak flank (help) and mobile ergonomics debt

Design Specificity Verdict

Authored for this product. Not category-interchangeable.

LLM assessment: The visual language is a committed editorial position, not a framework default. Specific choices that no generic CRUD would carry: a single 760px hairline column with border-bottom: 1px solid var(--rule) instead of cards (style.css:124,543); the ember law enforced in code and in comments — "neither ember … may say 'system unhealthy'" (style.css:236-241), busy bar deliberately grey (style.css:865); heat expressed typographically (hot title style.css:592, 2px ember underline style.css:525, ember wash style.css:548) with no pulse or badge bounce; the hatch + monogram cover fallback (style.css:121, card.html:18-19) built for scraped covers that 404; manga and novels as two separate libraries with the Updated tab existing only for manga because only manga has a poller-fed "what's out" (app.html:64-69, web.go:317); the Recent strip capped at 5 and suppressed on Updated to avoid duplicating the list (web.go:31,346).

What a generic app would keep unchanged: the underline tab row (style.css:374-403), the client-side title filter (filter.js:4-13), the admin table with select filters and pagination (series-list.html:9-33), the single OAuth button (login.html:22-26). That is a fair split — the IA is generic, the surface is not.

The one real breach of the committed system is admin: --measure-wide: 1080px (admin.css:2) plus a patina accent buys horizontal sprawl (7-column lane grid, admin.css:482) that collapses back to stacked rows at 899px anyway (admin.css:924). Width did not save the tables; it created a second visual system to learn.

Deterministic scan: detect.mjs --json backend/internal/web/templates → exit 0, zero findings ([]), verified across three runs. Pipeline liveness confirmed against a control snippet, which correctly returned exit 2 with overused-font. The scan covers templates only — CSS lives outside the scan root, so ai-color-palette, cream-palette and line-length rules never ran. Mechanical sweep instead:

  • Zero hardcoded colours outside the token blocks in either stylesheet. All 98 hex definitions sit in the dark :root (style.css:55-122) or the light branch (style.css:138-191). One non-token literal exists: rgba(0,0,0,.5) in a login-art drop-shadow (style.css:946). Every #… match in admin.css is an issue number in a comment.
  • Ember/danger segregation holds. 32 --ember lines and 26 --danger lines in style.css, no overlap; admin.css uses --ember zero times.
  • 0 z-index, 0 position: fixed, 2 !important (both justified: [hidden] override style.css:217, reduced-motion kill style.css:1060).
  • 0 injection sinks. filter.js writes through textContent only; no template.HTML/JS/URL anywhere in the package; the single raw write is escaped (web.go:425).
  • Accessibility is authored, not audited in: 36 real <button>s, 0 clickable <div>s, 0 hrefless <a>s, ~77 aria-* attributes, all 4 <img> intentionally alt="" beside a text name, 0 icon-only controls without an accessible name, 19 :focus-visible rules, lang + viewport + color-scheme on all three shells, one prefers-reduced-motion block.

Detector and review agree on the important thing: there are no mechanical anti-patterns here. Every finding below is a design judgment, not a lint.

Visual overlays: none. Browser visualization was skipped — the UI is only reachable through the Go/Docker stack, which was out of scope, so no local URL existed to point automation at. No overlay exists in your browser.

Overall Impression

This is the rare self-hosted tool with an actual design system that the code obeys. Tokens are complete in both colour branches, the ember law is enforced in comments and in the busy-state colour choice, destruction is gated and coloured correctly, and the accessibility work is native rather than retrofitted. The problems are not taste problems — they are mobile ergonomics and dead ends.

Biggest single opportunity: the phone. Mobile is stated as a hard functional constraint, but two of the three horizontal-scroll surfaces (Recent strip, tab row) hide their scrollbars and offer no fade, snap, or peek, so their off-screen content is simply invisible; and ~220px of chrome loads above the first card. The product's answer to "what do I read next" is the strip — and on a 360px viewport its newest item can sit off-screen with no hint that it exists.

What's Working

  1. The ember law is enforced where it is easiest to break. The busy indicator is grey (style.css:865-877), not ember; error is --danger, not ember; the code comment states the rule (style.css:236-241) and the token contrast is annotated with a measured ratio ("verified 5.31:1 on --ember-wash", style.css:73). Design systems fail at exactly this junction. This one holds.
  2. Out-of-band chrome truthfulness. Every mutation re-renders the strip, keyrow and Updated count out-of-band (web.go:397, chrome.html:10,35,75) and rebroadcasts a refilter event (filter.js:104), so the badge can never describe the pre-tap library. The comment at web.go:474-476 explicitly buys correctness with one extra read.
  3. Confirm gates focus the safe button. filter.js:155-157 deliberately puts focus on Cancel, not Remove — "pre-armed Enter is the opposite of what a confirm gate is for." Paired with the danger wash and the title quoted into the prompt (card.html:124), destruction is weighted correctly.

Priority Issues

[P1] Both horizontal-scroll surfaces hide their own overflow

  • Why it matters: .recent-strip (style.css:468-476) and .tabs (style.css:374-383) both set overflow-x: auto with scrollbar-width: none and a killed webkit scrollbar, and neither has a fade mask, scroll-snap, or a deliberately half-peeked item. At 93px covers plus 14px gaps (style.css:127,470), four cards fill a 360-390px viewport and the fifth — the newest — is invisible. Same for the fourth tab and its count pill. The strip is the product's answer to "what next"; an affordance nobody discovers is a feature that does not exist.
  • Fix: Add scroll-snap-type: x proximity on .recent-strip with scroll-snap-align: start per .recent-card, plus a right-edge mask-image gradient on both .recent and .tabs. Alternatively size the strip padding so one card is always half-visible. No JS.
  • Suggested command: /impeccable adapt

[P1] A guild outsider hits a dead end with no next step

  • Why it matters: The entire funnel is one page. login.html:16,28 says "Private library" and "Guild membership is required"; a non-member's refusal is "not a member of this community" (discord.go:185) with no guild name, no invite path, no "ask a member." Heuristic 10 scores 1 almost entirely on this. It is also the only screen a stranger ever sees.
  • Fix: Add one help line under login.html:28 naming the guild (deployment config, safe to display) and stating how to get in. Keep the existing distinction between a Discord outage and a membership refusal (web.go:171 vs 185) — that part is already right.
  • Suggested command: /impeccable clarify

[P2] Removal ends in silence for sighted users

  • Why it matters: Correction to Assessment A — the screen-reader path is not missing: web.go:592 already announces "Removed