fix: address code review on #27

- The empty state is about an empty library, not a brand-new Reader:
  listView.Fresh becomes EmptyLibrary and moves behind the tab-specific
  branches, so "No favourites yet" is no longer shadowed for a Reader whose
  library happens to be empty. The action key stays put — hiding it was
  never asked for.
- The owner is not a revocable Reader: their row offers no button and
  POST /readers/{owner}/revoke is a 404, so the one row where the control
  would sign out the tapping browser cannot be reached by a hand-rolled
  POST either.
- Modify isolation is asserted in both directions, and the owner's own
  sign-in through the OAuth callback is pinned to the seeded row.
- CUTOVER.md and REDEPLOY.md still grepped API_TOKEN out of .env for their
  smoke tests, which the last commit deleted; both now take the acting
  Reader's derived credential.
- Roster type follows the machine-fact spec (500 10-11px mono, tracked),
  and PRODUCT.md names the Readers panel instead of claiming there is no
  owner surface at all.
This commit is contained in:
2026-08-08 20:14:21 +07:00
parent b0bf6fe770
commit f4f6c9c9e9
10 changed files with 115 additions and 40 deletions
+2 -4
View File
@@ -30,11 +30,9 @@
{{/* The action key. The icon strip on a card is unlabelled, so one permanent
line under the tabs names every glyph. It follows the tab rather than the
row: the archived and finished buckets swap Archive for Restore, and a
finished series has no Done to offer. A Reader with no cards at all has
nothing for it to name, so it hides rather than disappearing — see the
note above about out-of-band swaps needing their target to exist. */}}
finished series has no Done to offer. */}}
{{define "keyrow"}}
<div class="keyrow" id="keyrow" aria-label="Action key"{{if .OOB}} hx-swap-oob="true"{{end}}{{if .Fresh}} hidden{{end}}>
<div class="keyrow" id="keyrow" aria-label="Action key"{{if .OOB}} hx-swap-oob="true"{{end}}>
<span class="pair"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-play"/></svg><span>Read</span></span>
<span class="pair brass"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-star"/></svg><span>Fav</span></span>
<span class="pair"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-pencil"/></svg><span>Chapter</span></span>
+11 -12
View File
@@ -8,18 +8,6 @@
<strong>No titles match “<span class="no-match-q"></span>”.</strong>
<button type="button" class="clear-search">Clear search</button>
</div>
{{else if .Fresh}}
{{/* Nothing anywhere, not an empty bucket: this Reader has just registered,
so the empty state is the setup instruction rather than a filter
report. Both scripts, because the two libraries are separate installs. */}}
<div class="empty">
<strong>Your library is empty.</strong>
<p>Install both userscripts, then open a series and read a chapter — bookmarks arrive on their own.</p>
<p class="setup-links">
<a class="ghost" href="/install/manga-bookmark.user.js">Install Manga script</a>
<a class="ghost" href="/install/novel-bookmark.user.js">Install Novels script</a>
</p>
</div>
{{else if eq .Tab "fav"}}
<div class="empty"><strong>No favourites yet.</strong><p>Star a series to pin it here.</p></div>
{{else if eq .Tab "new"}}
@@ -28,6 +16,17 @@
<div class="empty"><strong>Nothing archived.</strong><p>Shelve a series to park it here — it keeps getting checked for new chapters.</p></div>
{{else if eq .Tab "finished"}}
<div class="empty"><strong>Nothing finished yet.</strong><p>Mark a series finished and it moves out of your reading list.</p></div>
{{else if .EmptyLibrary}}
{{/* Nothing in either library, so the links are the only thing this page can
usefully say. Both scripts: the two libraries are separate installs. */}}
<div class="empty">
<strong>Nothing here yet.</strong>
<p>Install the userscripts, then open a series and read a chapter — bookmarks arrive on their own.</p>
<p class="setup-links">
<a class="ghost" href="/install/manga-bookmark.user.js">Install Manga script</a>
<a class="ghost" href="/install/novel-bookmark.user.js">Install Novels script</a>
</p>
</div>
{{else}}
<div class="empty"><strong>Nothing here yet.</strong><p>Bookmarks appear once the userscript records a chapter.</p></div>
{{end}}
+4 -1
View File
@@ -13,7 +13,10 @@
<li>
<span class="reader-id">{{.DiscordID}}</span>
<span class="reader-sessions">{{.Sessions}} session{{if ne .Sessions 1}}s{{end}}</span>
{{if .Sessions}}
{{/* 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>