Files
mangaBookmark/plans/2026-07-25-bookmark-list-favorites-design.md
T
sulthan 0f7d611a60 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>
2026-07-25 11:28:54 +07:00

164 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.