Final-review fix wave over the web UI branch.
- sessionKey now derives from API_TOKEN and WEB_PASSWORD with a \x00
separator, so rotating the password logs every browser out too.
- uiChapter only clears last_chapter_url when the number actually
changes. The form is pre-filled, so a bare tap of Save resubmits the
same value; that used to destroy the chapter URL silently while
updated_at stayed put, degrading Continue to the series index page.
- MANGA_WEB_HOST is now required by the prod override rather than
falling back to manga.example.com, matching MANGA_API_HOST.
- Comment fixes: static cache rationale, pruneLocked aliasing
invariant, and the stale "3 routes" line in CLAUDE.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix backend/Dockerfile to COPY templates/ and static/ (the go:embed
assets from Tasks 5-7) alongside *.go, plus backend/.dockerignore which
was silently excluding both directories from the build context — the
Dockerfile fix alone still failed the build. Wire WEB_PASSWORD through
docker-compose.yml, add a second Traefik router (mangaweb) plus explicit
service labels on both routers in docker-compose.prod.yml, and document
the new variables and deploy steps in .env.example, DEPLOY.md, and
CLAUDE.md.
Also fixes a CSS specificity bug found during manual browser testing:
.chapter-form { display: flex } has the same specificity as the browser's
built-in [hidden] { display: none } rule and wins by cascade order, so the
per-card chapter-edit form stayed visible even with the hidden attribute
set. Added .chapter-form[hidden] { display: none } alongside the existing
.card[hidden] override.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
hx-target="#card-<key>" is an invalid CSS selector for any key containing
a colon (every real bookmark key is "<site>:<series_id>"), so htmx threw
before swapping and the favourite/delete/chapter-override controls were
dead in the browser. Switch to the attribute-selector form
[id='card-<key>'], which querySelectorAll accepts regardless of the id's
characters.
Also close a validation gap in uiChapter: strconv.ParseFloat accepts
"NaN"/"Infinity"/"-Inf" with err == nil, and every comparison against NaN
is false, so num < 0 let both through to last_chapter_num and permanently
broke HasNewChapter. Reject non-finite values explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the three UI mutation endpoints (favourite toggle, manual chapter
override, delete) plus the card controls that call them via htmx.
Each mutation is a read-modify-write through Store.Get/Upsert so
Upsert alone decides whether updated_at moves — favouriting must not
reorder the list, only real reading progress should.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds clientIP() (reads the rightmost X-Forwarded-For hop via
Header.Values, since Traefik appends the peer address it actually
observed and the leftmost entries are client-controlled) and
loginLimiter, an in-memory per-IP counter that blocks after
loginMaxFailures within loginWindow. No routes wire these up yet —
that lands in Task 5.
Adds sessionKey/signSession/verifySession primitives and
setSessionCookie/clearSessionCookie helpers in a new backend/session.go.
Sessions are derived from API_TOKEN via HMAC-SHA256 with domain
separation (sessionKeyPurpose), so there is no session table and
rotating the token invalidates every outstanding cookie at once.
No routes or handlers yet — that's task 5.
Adds a browser-accessible bookmark list on a new subdomain, served by the
existing Go binary via go:embed'd templates and htmx. Cookie sessions
(HMAC-keyed off API_TOKEN, 60-day) gate /ui/*; the bearer-authenticated
/bookmarks* API and the userscript are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Documents why updated_at moves only on reading progress and why PUT therefore
returns the stored row, why "latest chapter" is found from the browser rather
than the backend, and its limits — a bookmark is as current as its last check,
and nothing here can be instant.
Also corrects two stale claims: asurascans.com is the current domain, and
asuracomic.net deep links now 301 to its root rather than the matching path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bookmarks now show the newest chapter a site has published alongside the one
the user has read. A series page carries its whole chapter list, so standing on
one records it directly; everything else is learned by fetching series pages in
the background, one per navigation and at most every four hours per series.
Those fetches are same-origin on purpose — they ride the browsing session that
gets past the sites' bot checks, which a request from the backend could not.
Freshness is tracked per device in localStorage rather than synced, since each
device checks independently.
Favourites are a synced flag with a star toggle and a second tab. Filtering
happens at render time, so a favourited series still appears under All.
Neither favouriting nor recording a new chapter reorders the list: both send
updated_at only as a candidate, and the server keeps the stored value unless
reading progress moved.
Also corrects the asuracomic.net comment — those deep links now 301 to the
asurascans.com root, dropping the path, before any script runs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds favorite, latest_chapter and latest_chapter_num to the bookmark record,
with an idempotent ALTER TABLE migration so the already-deployed database
picks them up.
updated_at now moves only when a bookmark is new or last_chapter_num changes.
Clients order their list by updated_at, so favoriting a series or recording a
newly published chapter must not disturb that order. Upsert consequently
returns the row as stored and the handler echoes that rather than the request
payload, since the candidate timestamp it sends is often discarded.
Scanning also tolerates NULL in the optional columns, which a database created
before this code can legitimately contain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Concrete backend + userscript plan against the approved design doc, including
the Store.Upsert return-value fix needed to keep ordering correct once
updated_at becomes conditional, and background-refresh triggering on both
init() and SPA navigation per user preference.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same-origin fetch() from the userscript, throttled per-bookmark, to reduce
the "only fresh when you open the exact series page" gap without server-side
polling (still blocked by Cloudflare). Also documents that no JSON API or
RSS feed exists on either site, ruling out a more stable poll target.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Ignore graphify-out/ (local graph data) and document query/update
workflow in CLAUDE.md for future codebase questions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auto-update no longer fires the instant a newer chapter opens (guards against
a misclick on "latest chapter"). Instead a 25s dwell timer arms, shown by a
countdown ring filling around the FAB; the manual "Update to X" button still
fires immediately. Timer is keyed to the chapter, not the URL, so turning
pages within the same chapter (Demonic /chapter/N/<page>) keeps it counting
rather than resetting.
FAB is now draggable: drag anywhere, release snaps it to the nearer left/right
edge keeping its vertical position, persisted in localStorage across sessions
and re-clamped on rotation. A drag no longer opens the panel; a tap still does.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
init() ran at line 514 (document.body exists at @run-at document-idle),
but buildUI() reads the TEMPLATE/CSS consts declared lower in the IIFE.
Accessing them before initialization threw ReferenceError: Cannot access
'CSS' before initialization, so the script died before mounting the FAB
and no UI appeared in Cromite. Move the boot invocation to the end of the
IIFE, after both consts are initialized.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add traefik.enable + Host/entrypoints/tls/certresolver/service labels to the
prod override, driven by MANGA_API_HOST / PROXY_NETWORK / TRAEFIK_ENTRYPOINT /
TRAEFIK_CERTRESOLVER env vars (documented in .env.example).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>