e69da2a99b
The only operational surface was /healthz and a fold-out roster inside the owner's own reading page. This gives the owner a page: two sections of facts on the same measured sheet, no cards. - admin.go holds every route that reaches past the acting Reader, listed once in adminRoutes() and wrapped in requireOwner at registration - a missing gate is visible in the route list rather than hidden inside a handler. A non-owner gets 404, the same answer revoke already gave. - Lane figures arrive through the LaneReporter seam, so the page reads the running poller rather than a table. newRouter converts a nil *Poller to a nil interface: a typed nil would make the page claim a poller exists. - The roster moves out of the reading page and gains the Sighting counters, the blocked verdict and Clear marks. Clearing restores a privilege, so it is a plain ghost button; --danger stays with revocation. - --patina is the page's one accent, held at the weight of the other action accents. Neither --ember (new chapter) nor --danger (destruction) is borrowed for system health.
49 lines
2.6 KiB
HTML
49 lines
2.6 KiB
HTML
{{/* The Reader roster, on the owner's administrative page. Re-rendered whole
|
|
as the response to an action so the counts and marks it shows cannot
|
|
describe the state before the tap. Both actions are confirm-gated: one
|
|
signs a Reader out of every device at once, the other wipes a record.
|
|
|
|
Owner-only at route registration, so nothing here re-tests who is asking. */}}
|
|
{{define "readers"}}
|
|
<section class="readers" id="readers">
|
|
<h2>Readers</h2>
|
|
<p class="setup-copy">Everyone who has signed in through Discord. Revoking
|
|
signs a Reader out of every device; their library and bookmarks are
|
|
untouched, and they can sign in again. The Sighting counters record how
|
|
often a later Poll confirmed or contradicted what that Reader's browser
|
|
reported; enough contradictions stop their reports deferring a Poll, and
|
|
clearing the marks gives that back.</p>
|
|
<ul class="readerlist">
|
|
{{range .Readers}}
|
|
<li>
|
|
<span class="reader-id">{{.DiscordID}}</span>
|
|
<span class="reader-sessions">{{.Sessions}} session{{if ne .Sessions 1}}s{{end}}</span>
|
|
<span class="reader-sightings">{{.Agreements}} confirmed / {{.Disagreements}} contradicted</span>
|
|
{{/* Blocked is spelled out rather than left to be worked out from two
|
|
numbers and a threshold. */}}
|
|
{{if .Blocked}}<span class="reader-blocked">deferral blocked</span>{{end}}
|
|
<span class="reader-actions">
|
|
{{/* Clearing restores a privilege, so it is a plain ghost button —
|
|
the destruction accent belongs to revocation alone. It is offered
|
|
on every row, including one reading zero: the remedy must be
|
|
findable before the counters climb, not after. */}}
|
|
<form hx-post="/readers/{{.ID}}/clear-marks" hx-target="#readers" hx-swap="outerHTML"
|
|
hx-confirm="Clearing wipes this Reader's whole Sighting record, confirmations included. Clear?">
|
|
<button type="submit" class="ghost">Clear marks</button>
|
|
</form>
|
|
{{/* The owner's own row never offers Revoke: it is the one row where the
|
|
button would sign the tapping browser out, and the endpoint refuses
|
|
it anyway. Logout is the deliberate way to do that. */}}
|
|
{{if and .Sessions (ne .ID $.OwnerID)}}
|
|
<form hx-post="/readers/{{.ID}}/revoke" hx-target="#readers" hx-swap="outerHTML"
|
|
hx-confirm="Revoking signs this Reader out on every device immediately. Revoke?">
|
|
<button type="submit" class="ghost danger">Revoke sessions</button>
|
|
</form>
|
|
{{end}}
|
|
</span>
|
|
</li>
|
|
{{end}}
|
|
</ul>
|
|
</section>
|
|
{{end}}
|