Re-review of the fix wave found the .env.example edit defeated the fix
it belonged to: shipping MANGA_WEB_HOST=manga.example.com re-supplied
the value that ${MANGA_WEB_HOST:?} exists to reject, so a fresh
`cp .env.example .env` started fine and Traefik published the UI router
on a domain the operator does not own. Left commented, matching
MANGA_API_HOST; DEPLOY.md 1 now lists it among the required variables.
Also:
- uiChapter leaves last_chapter too, not only last_chapter_url, when the
submitted number is unchanged. It used to rewrite the display string
("45.0" to "45") behind a frozen updated_at.
- Design spec 4.2 documents the two-secret key derivation.
- Corrected the pruneLocked aliasing rationale and the stale
sessionKeyPurpose comment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>