Commit Graph

65 Commits

Author SHA1 Message Date
sulthan ebc7a546c5 feat: password-gated web UI on the same backend (#1)
Adds a password-gated browser UI for the bookmark list, served by the same Go
binary and container as the userscript API.

## What

- `GET /` — list page, or the login page when there is no session (200, no redirect).
- `POST /login`, `POST /logout` — stateless HMAC session cookie, 60-day Max-Age.
- `GET /ui/list?tab=all|fav`, `POST /ui/bookmarks/{key}/favorite`,
  `POST /ui/bookmarks/{key}/chapter`, `DELETE /ui/bookmarks/{key}` — htmx fragments.
- `GET /static/*` — embedded `style.css`, `htmx.min.js`, `filter.js`.

Mobile-first dark CSS, 2–3 column grid at ≥900px, "Continue reading" strip of the
five most recent series, NEW badge, client-side title search, no build step.

## Stack

Go `html/template` + htmx 2.0.4 (vendored, 50 KB) + plain CSS. No npm, no bundler.
Templates and assets are `go:embed`-ed, so `CGO_ENABLED=0` and the distroless
image still hold.

## Auth

`WEB_PASSWORD` gates the UI; unset means the web routes are never registered and
`/` returns 404. Session cookie is `HttpOnly`, `SameSite=Lax`, `Secure` when the
request is HTTPS. The signing key derives from `API_TOKEN` + `WEB_PASSWORD`, so
rotating either logs every browser out. Login is rate-limited to 10 failures per
20 minutes per client IP, keyed on the **rightmost** `X-Forwarded-For` entry
(Traefik appends the observed peer, so the leftmost is client-spoofable). CGNAT
lockout is a known, accepted limitation — the window self-heals.

## Invariants preserved

- A session cookie never authenticates `/bookmarks*`. That API stays JSON +
  bearer token, unchanged, as does the userscript.
- `Store.Upsert` is byte-for-byte unmodified. Every UI write goes
  read-modify-write through the new `Store.Get`, so the conditional-`updated_at`
  rule (favouriting must not reorder the list, a chapter override must) lives in
  exactly one function.

## Deployment

`docker-compose.prod.yml` gains a second Traefik router on `MANGA_WEB_HOST`
pointing at the same service — one container, one certificate resolver, no second
service. Both `MANGA_API_HOST` and `MANGA_WEB_HOST` are required (`:?`), with no
example fallback in `.env.example`: a placeholder there would make Traefik
silently publish the UI on a domain you do not own. Needs a DNS A/AAAA record for
`manga.<domain>`. See `DEPLOY.md` §1b.

## Docs

- Design: `docs/superpowers/specs/2026-07-25-web-ui-design.md`
- Plan: `plans/2026-07-25-web-ui-implementation-plan.md`

## Verification

`gofmt` clean, `go vet`, `go test -race ./...`, `CGO_ENABLED=0 go build`, a real
`docker build` + curl smoke test, and a Playwright pass covering login
reject/accept, favourite-without-reorder, chapter edit, delete-with-confirm,
search, tab switch + back button, 390px with no horizontal overflow, and zero JS
console errors.

Reviewed-on: #1
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
2026-07-26 03:59:58 +07:00
sulthan c2914d0b5a docs: record conditional updated_at, latest-chapter tracking, favourites
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>
2026-07-25 12:12:12 +07:00
sulthan 6df8836334 feat(userscript): latest-chapter tracking, favourites, All/Favourites tabs
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>
2026-07-25 12:10:40 +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
sulthan 053355d0c6 docs: add implementation plan for bookmark reorder/latest-chapter/favorites
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>
2026-07-25 12:01:10 +07:00
sulthan 83c3ffe3ad docs: add background opportunistic refresh to latest-chapter design
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>
2026-07-25 11:40:17 +07:00
sulthan 0f7d611a60 docs: design for bookmark reorder-by-progress, latest-chapter display, favorites
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>
2026-07-25 11:28:54 +07:00
sulthan d1e0d7bf19 docs: add graphify knowledge graph integration
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>
2026-07-25 11:05:22 +07:00
sulthan 2e4a4519f0 feat(userscript): 25s dwell before auto-update + draggable edge-snap FAB
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>
2026-07-24 20:07:39 +07:00
sulthan c597bb5d5b fix(userscript): move boot after TEMPLATE/CSS consts to avoid TDZ crash
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>
2026-07-24 18:46:10 +07:00
sulthan 0ef528648d change the domain and add the api key to the script 2026-07-24 18:18:22 +07:00
claude cb95cef763 docs: add DEPLOY.md step-by-step (Traefik + Bromite)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:12:34 +07:00
claude 894a421a9d feat: Traefik router labels for prod deploy
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>
2026-07-24 17:04:31 +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
claude d47a07af46 chore: initial plan 2026-07-24 16:23:24 +07:00