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 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>