docs: design for bookmark reorder-by-progress, latest-chapter display, favorites

Covers list reordering (already implemented, no-op confirmed), latest-available-chapter
capture on series-page visits, favorites synced via backend, a required backend change
to make updated_at conditional on progress advance, and the asuracomic.net redirect
regression found while verifying feasibility live.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 11:28:54 +07:00
parent d1e0d7bf19
commit 0f7d611a60
@@ -0,0 +1,163 @@
# Bookmark list ordering, latest-chapter display, and favorites
Date: 2026-07-25
Status: Approved by user, pending implementation plan
## Context
Three requested additions to the Bromite userscript's bookmark panel
(`userscript/manga-bookmark.user.js`) plus one incidental bug found while
verifying feasibility live:
1. Bookmark list should show the most-recently-read manga first.
2. Show not just the last chapter *read*, but the latest chapter *available*
for that manga, if feasible.
3. A favorites mechanism: a second list/tab showing only favorited manga,
without removing favorited manga from the normal list.
## 1. Reorder by latest read (already implemented)
`reindex()` in `manga-bookmark.user.js` has sorted `state.list` by
`(b.updated_at || 0)` descending since the very first commit
(`f58d113`). `updated_at` is set whenever progress is recorded
(`bookmarkCurrent`, `updateToCurrentChapter`, `setChapterManual`). No code
change needed here — confirmed by decision: only an actual progress advance
should reorder the list (not simply opening/re-reading an old chapter).
The only risk to this existing behavior is introduced by features 2 and 3
below, since both add new fields synced through the same PUT endpoint that
currently (per existing CLAUDE.md) has the server set `updated_at`
unconditionally on every upsert. See section 4.
## 2. Latest available chapter
### Feasibility (verified live via Playwright, 2026-07-25)
- **Chapter reader pages only expose immediate neighbors.** On
`asurascans.com/comics/dungeon-odyssey-f886a8af/chapter/160`, the DOM
contains only chapters 159–161 (prev/next nav) — not the full list.
- **Series pages list every chapter, newest first by default.** On
`asurascans.com/comics/dungeon-odyssey-f886a8af`, the series page's chapter
list panel contains one `<a href=".../chapter/N">Chapter N ...</a>` per
chapter (verified: 162 links for a 162-chapter series, first is Chapter
162). This is therefore the only reliable place to learn the true latest
chapter number.
- Demonic's series page (`demonicscans.org/manga/Dungeon-Odyssey`) similarly
lists every chapter via `chaptered.php?manga=<id>&chapter=<n>` links,
interleaved with a "first chapter" quick-jump link. Taking `max()` of all
parsed chapter numbers (rather than assuming list order) is used for
robustness on this site.
**Conclusion: latest-available-chapter data can only be refreshed when the
user visits a bookmarked series' series page** (not on every chapter open).
This is a real, disclosed limitation — not solved by polling, since the
backend cannot fetch these sites itself (Cloudflare blocks server-side
fetch, per existing CLAUDE.md constraint) and there is no reader-page source
for it.
### Behavior
- New adapter capability: on `detect()` returning `type: "series"` for an
already-bookmarked series (`state.byKey[key]` exists), scan the page for
chapter links per the site-specific pattern below and compute the max
chapter number + its display label.
- Asura: `a[href*="/chapter/"]` where the href matches
`/chapter/([\d.]+)$/` and the link text matches `/Chapter\s+[\d.]+/`
(excludes the unrelated "First Chapter" quick-jump button, which lacks
that text pattern).
- Demonic: all `a[href]` matching
`/chaptered\.php\?manga=\d+&chapter=([\d.]+)/`; take the max parsed
chapter number across all matches (list order is not assumed reliable).
- If the computed max differs from the bookmark's stored
`latest_chapter_num`, silently update `latest_chapter` (label) and
`latest_chapter_num` via the API — but this update must **not** change
`updated_at` / list order (see section 4).
- Display: each list item's subtitle line becomes e.g.
`"Read: Chapter 12 · Latest: Chapter 15"` when latest is known and differs
from last-read; otherwise unchanged (`"Chapter 12 · asura"` as today).
Plain inline text, no separate badge/count UI.
## 3. Favorites
- New field `favorite: bool` on the bookmark record, synced through the
existing PUT endpoint (chosen over local-only storage so favorites persist
across devices/reinstalls, consistent with how the rest of the data
syncs).
- Each list item gets a star toggle (☆ / ★) that flips `favorite` and PUTs
the updated bookmark. Toggling **must not** change `updated_at` / list
order (see section 4).
- The panel gains two tabs above the bookmark list: **All** and
**★ Favorites**. Both apply the same sort (section 1). Switching tabs is
local UI state (not persisted) defaulting to "All". A favorited manga
continues to appear in "All" — tabs only filter which array is rendered,
favoriting never removes the bookmark from `state.list`.
## 4. Backend change required: conditional `updated_at`
Current CLAUDE.md / `store.go` behavior: `PUT /bookmarks/{key}` always sets
`updated_at` server-side on every upsert. Once latest-chapter-capture and
favorite-toggle both PUT through that same endpoint, this would reorder the
list on every series-page visit or star click — contradicting the
"reorder only on progress advance" decision from section 1.
**Change:** `Store.Upsert` sets `updated_at = now()` only when:
- the bookmark is new (no existing row for that key), or
- `last_chapter_num` in the incoming payload differs from the currently
stored value.
Otherwise the existing stored `updated_at` is preserved, even though other
fields (`favorite`, `latest_chapter`, `latest_chapter_num`, cover, title,
etc.) are still updated. This centralizes "what counts as a progress
advance" as a single authoritative rule in the backend, applied consistently
regardless of which device/browser performed the write.
This is a deliberate deviation from the current CLAUDE.md wording ("server
sets `updated_at`" unconditionally) and needs the doc updated to match.
## 5. Asura redirect bug (bundled into this work)
Verified live: `https://asuracomic.net/comics/dungeon-odyssey-f886a8af`
returns an HTTP **301** with `Location: https://asurascans.com/` (root, no
path) — a Cloudflare-edge redirect that discards the path *before any JS on
asuracomic.net executes*. This contradicts the existing code comment
("asuracomic.net currently 301s to asurascans.com", implying path
preservation) and means any deep link on `asuracomic.net` currently lands
the user on the asurascans.com homepage with page type `"other"` —
bookmark/progress detection silently does nothing.
There is no client-side fix: the userscript's `@match` for
`asuracomic.net/*` never gets a chance to run for these URLs, since the
redirect happens at the edge before the browser has a document to inject
into. **Fix is documentation-only**: correct the misleading comment in
`asura.matches()`/adapter notes and README to state that `asuracomic.net`
deep links are currently broken, and the user should navigate via
`asurascans.com` links directly. No behavior change to ship.
## Data model summary
`Bookmark` (backend `store.go`) gains:
| field | type | notes |
|---|---|---|
| `favorite` | `bool` | default `false` |
| `latest_chapter` | `string` | display label, e.g. `"Chapter 162"`; empty if never captured |
| `latest_chapter_num` | `*float64` | nullable; null if never captured |
SQLite migration: additive `ALTER TABLE bookmarks ADD COLUMN ...` for each,
guarded against "duplicate column" errors so it's safe to run against the
already-deployed database.
## Testing
- Go: table-driven tests for `Store.Upsert`'s conditional `updated_at` logic
(new bookmark, unchanged progress, changed progress, favorite-only change,
latest-chapter-only change).
- Userscript: manual on-device verification (per existing project
convention — no JS test harness in this repo). Verify:
- list reorders only when a chapter is actually advanced, not on
plain re-visit or favorite toggle.
- latest-chapter capture fires on series-page visits and displays
correctly for both sites.
- favorite toggle persists across a panel close/reopen and a
`refresh()` (i.e. round-trips through the backend correctly).
- favorited manga still appears in "All" tab.