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:
@@ -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.
|
||||||
Reference in New Issue
Block a user