Files
mangaBookmark/.impeccable/critique/2026-08-27T14-36-34Z__backend-internal-web.md
sulthan af07314bb6 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.
2026-08-27 23:05:40 +07:00

19 KiB
Raw Permalink 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