Commit Graph

13 Commits

Author SHA1 Message Date
sulthan edae491161 fix: keep MANGA_WEB_HOST unset in .env.example and make no-op saves inert
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>
2026-07-26 03:27:31 +07:00
sulthan 22bf68f12f fix(backend): bind session key to both secrets and guard no-op chapter saves
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>
2026-07-26 03:18:00 +07:00
sulthan aac00ec01c chore: build, route, and document the web UI
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.
2026-07-25 23:41:43 +07:00
sulthan 8c7d3c9c46 feat(backend): mobile-first styling and client-side title search
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>
2026-07-25 23:31:58 +07:00
sulthan daec18546c fix(backend): valid hx-target selector and reject NaN/Infinity chapter input
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>
2026-07-25 23:19:16 +07:00
sulthan 2c3d687b6d feat(backend): favourite, chapter override, and delete fragments
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>
2026-07-25 23:10:55 +07:00
sulthan f70707f154 feat(backend): password login, session gate, and list page
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:03:00 +07:00
sulthan e3d86e826c chore(backend): vendor htmx 2.0.4 and add WEB_PASSWORD config 2026-07-25 22:57:40 +07:00
sulthan 6030a985fa feat(backend): per-IP login rate limit with proxy-aware client IP
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.
2026-07-25 22:52:22 +07:00
sulthan 7d0eaaef02 feat(backend): stateless HMAC session cookies for the web UI
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.
2026-07-25 22:45:52 +07:00
sulthan e42b30057f feat(backend): add Store.Get and bookmark view helpers 2026-07-25 22:42:14 +07:00
sulthan 01301805fb feat(backend): favorite + latest-chapter fields, conditional updated_at
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>
2026-07-25 12:06:44 +07:00
claude f58d113934 feat: manga bookmark sync backend + Bromite userscript
Backend (Go, stdlib net/http + modernc.org/sqlite, CGO-free static binary):
- GET/PUT/DELETE /bookmarks{,/key} + /healthz
- bearer auth (constant-time), CORS origin reflection + 204 preflight
- SQLite store keyed <site>:<series_id>, last-write-wins, server-set updated_at
- httptest + temp-sqlite tests (auth, CORS, round-trip); go vet clean
- multi-stage Dockerfile (distroless static nonroot) + compose (base + prod proxy override)

Userscript (single Bromite-compatible IIFE, no GM_* APIs):
- Asura + Demonic adapters, URL-regex ids + og: title/cover
- Shadow-DOM floating UI, localStorage cache, optimistic sync
- auto-progress (no regress) + manual override; framework-agnostic nav watcher

Live-verified adapters (Playwright, 2026-07-24): asurascans.com /comics/<slug-hash>,
demonicscans.org /manga/<slug> + /title/<slug>/chapter/<n> — corrects the plan's
assumed /series/ paths and asuracomic.net domain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 16:48:28 +07:00