From 0f7d611a60aa553edec2f8c4540987cc5c5d067b Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Sat, 25 Jul 2026 11:28:54 +0700 Subject: [PATCH] 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 --- ...26-07-25-bookmark-list-favorites-design.md | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 plans/2026-07-25-bookmark-list-favorites-design.md diff --git a/plans/2026-07-25-bookmark-list-favorites-design.md b/plans/2026-07-25-bookmark-list-favorites-design.md new file mode 100644 index 0000000..380380e --- /dev/null +++ b/plans/2026-07-25-bookmark-list-favorites-design.md @@ -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 `Chapter N ...` 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=&chapter=` 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.