Files
mangaBookmark/backend/internal/web/templates/lanes.html
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

64 lines
3.8 KiB
HTML

{{/* Poll Lane status: one row per Site's latest durable pass, refreshing
itself so a run can be watched rather than sampled by reloading. The
refresh is one attribute on the fragment root and the endpoint answers
with this same fragment, so the swap replaces the element that asked.
Every figure is read from poll_passes, never from a running poller: a
restart answers from the database the moment it is up (issue #145). The
browser fact is a deployment-config fact plus a reachability derived from
the pass log; the cause chips and the state phrase are decided in Go,
this template only prints them. */}}
{{define "lanes"}}
<section class="lanes" id="lanes"
hx-get="/ui/admin/lanes" hx-trigger="every 30s" hx-swap="outerHTML" hx-sync="this:replace">
<div class="sechead">
<h2 class="sec">Poll Lanes</h2>
<p class="statusline">
{{if .PollerOff}}Polling: <span class="mark-faint">off</span>
{{else}}Browser: {{if not .BrowserConfigured}}<span class="mark-faint">not configured</span>{{else if .BrowserReachable}}<span class="mark mark-strong">reachable</span>{{else}}<span class="mark bad">unreachable</span>{{end}}{{end}}
</p>
</div>
<p class="hint">Healthy idle (paused, browser asleep, nothing eligible) needs no action — attention needs the owner.</p>
{{if .Rows}}
<div class="tbl lanes">
<div class="thead">
<span>Site</span><span>Due</span><span>Checked</span><span>Gap</span>
<span>Last pass</span><span title="Window outcomes for this Site (last 12h)">Outcomes · status</span><span></span>
</div>
{{range .Rows}}
<div class="trow{{if .Attention}} attention{{end}}">
<a class="c-site" href="{{.FailingHref}}">{{.Site}}</a>
{{/* data-label is the column word for the phone layout, which drops the
thead: three bare numbers in a row say nothing. */}}
<span data-label="due">{{.Due}}</span>
<span data-label="checked">{{.Checked}}</span>
<span data-label="gap">{{.Gap}}</span>
<span>ran {{.Ran}}</span>
<span class="c-skip">{{if .HasChips}}{{range $i, $c := .Chips}}{{if $i}}<span class="mark-faint"> · </span>{{end}}<span class="mark">{{$c.Name}} {{$c.Count}}</span>{{end}}{{else}}<span class="mark-faint">none observed</span>{{end}}{{if .StatePhrase}} · <span class="{{if .StateGood}}ok{{else}}bad{{end}}" aria-label="{{if .StateGood}}healthy: {{end}}{{.StatePhrase}}">{{.StatePhrase}}</span>{{end}}</span>
{{/* 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
takes nothing away and reverses in one press — so neither wears a
confirm row or the danger accent. The form wraps the select so the
offered duration travels with the press. */}}
<span class="c-ctrl">{{if .Paused}}<span class="pausebar">
<form hx-post="/admin/lanes/{{.Site}}/resume" hx-target="#lanes" hx-swap="outerHTML" hx-indicator="#lanes" hx-disabled-elt="find button" hx-sync="closest #lanes:replace">
<button type="submit" class="ghost">Resume</button>
</form>
</span>{{else}}<span class="pausebar">
<form hx-post="/admin/lanes/{{.Site}}/pause" hx-target="#lanes" hx-swap="outerHTML" hx-indicator="#lanes" hx-disabled-elt="find button, select" hx-sync="closest #lanes:replace">
<select name="duration" aria-label="Pause duration">
<option>1h</option><option selected>6h</option><option>24h</option>
</select>
<button type="submit" class="ghost">Pause</button>
</form>
</span>{{end}}</span>
</div>
{{end}}
</div>
{{else}}
<p class="empty">No data yet — no Lane has recorded a pass.</p>
{{end}}
</section>
{{end}}