0f7d611a60
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>
164 lines
7.9 KiB
Markdown
164 lines
7.9 KiB
Markdown
# 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.
|