Add comix.to and kagane.to support #13

Merged
sulthan merged 17 commits from feat/add-kagane-comix into main 2026-08-03 19:53:46 +07:00
Owner

Tracks read progress on comix.to and kagane.to alongside asura and demonic, in both the userscript and the backend.

Implements docs/superpowers/plans/2026-08-03-comix-kagane-support.md.

Userscript

  • comix adapter — /title/<id>-<slug>; only the id prefix is identity (the slug follows the title). No og:image, so the cover is matched by alt.
  • kagane adapter — reader URLs are uuids with no chapter number, so it comes out of og:title; anchor scanning is structurally impossible, replaced by latestChapterFromApi against kagane's same-origin JSON API.
  • seriesId threaded through latestChapterFromAnchors so comix can scope its scan to its own series and a recommendation strip cannot win the maximum.
  • @match for both hosts, panel chips, v1.6.0.

Backend

  • latestChapterFrom cases: comix parses the SSR JSON state blob (latestChapterUrl, scoped to the series id); kagane parses API JSON (chapter_no).
  • Poller allowlist extended; Poller.BrowserFetch with fetcherFor(site) routes kagane to a browser fetcher. Nil means kagane is not polled at all — never a fallback to the TLS fetcher, which would only ever retrieve a challenge page.
  • BrowserFetcher: chromedp against a headless-shell sidecar. kagane sits behind a Cloudflare JS challenge that no TLS fingerprint clears, and the request is made inside the page rather than by replaying cf_clearance.
  • BROWSER_WS_URL wiring, sidecar in both compose files (no ports:, dedicated non-external network), Dockerfile on golang:1.26-alpine — chromedp requires go 1.26.
  • Web UI --comix / --kagane tokens in both colour branches.

Notes for review

  • series_url is client-supplied and a headless browser is a strong SSRF primitive, so kagane's host is pinned twice: in fetchableSeriesURL and again in kaganeAPIURL.
  • Three chained defects found during verification made the browser path dead under Compose (sidecar flag collision, Chrome's Host-header DNS-rebinding check, the wrong chromedp option). Fixed; the compose comments record the wrong configurations too, so they don't get "simplified" back.
  • ALLOWED_ORIGINS now includes both new origins. Without it every write from comix/kagane silently fails CORS preflight, parks in the retry queue, and drops at the cap.

Verification

221 backend tests, 32 userscript tests, static CGO_ENABLED=0 build, both compose configs.

Two gaps, both real:

  1. The userscript on live pages via Violentmonkey needs a human browser profile — not run. Check: comix series page (title/cover, no chapter), comix chapter page (records the number; an older chapter must not regress it), comix SPA navigation without reload, kagane series page (og:image cover), kagane reader (number from og:title), both chips opening the right sites.
  2. The kagane browser path has not completed end-to-end anywhere. Dial/navigate/fetch is confirmed, but Cloudflare 403'd headless-shell's Chrome on every attempt from the dev sandbox, and comix's poll-through-Docker was blocked by that environment's TLS interception. Both environment-dependent rather than branch defects — the first real deploy is the actual verification.
Tracks read progress on comix.to and kagane.to alongside asura and demonic, in both the userscript and the backend. Implements `docs/superpowers/plans/2026-08-03-comix-kagane-support.md`. ## Userscript - `comix` adapter — `/title/<id>-<slug>`; only the id prefix is identity (the slug follows the title). No `og:image`, so the cover is matched by `alt`. - `kagane` adapter — reader URLs are uuids with no chapter number, so it comes out of `og:title`; anchor scanning is structurally impossible, replaced by `latestChapterFromApi` against kagane's same-origin JSON API. - `seriesId` threaded through `latestChapterFromAnchors` so comix can scope its scan to its own series and a recommendation strip cannot win the maximum. - `@match` for both hosts, panel chips, v1.6.0. ## Backend - `latestChapterFrom` cases: comix parses the SSR JSON state blob (`latestChapterUrl`, scoped to the series id); kagane parses API JSON (`chapter_no`). - Poller allowlist extended; `Poller.BrowserFetch` with `fetcherFor(site)` routes kagane to a browser fetcher. Nil means kagane is not polled at all — never a fallback to the TLS fetcher, which would only ever retrieve a challenge page. - `BrowserFetcher`: chromedp against a `headless-shell` sidecar. kagane sits behind a Cloudflare JS challenge that no TLS fingerprint clears, and the request is made inside the page rather than by replaying `cf_clearance`. - `BROWSER_WS_URL` wiring, sidecar in both compose files (no `ports:`, dedicated non-external network), Dockerfile on `golang:1.26-alpine` — chromedp requires go 1.26. - Web UI `--comix` / `--kagane` tokens in both colour branches. ## Notes for review - `series_url` is client-supplied and a headless browser is a strong SSRF primitive, so kagane's host is pinned twice: in `fetchableSeriesURL` and again in `kaganeAPIURL`. - Three chained defects found during verification made the browser path dead under Compose (sidecar flag collision, Chrome's Host-header DNS-rebinding check, the wrong chromedp option). Fixed; the compose comments record the wrong configurations too, so they don't get "simplified" back. - `ALLOWED_ORIGINS` now includes both new origins. Without it every write from comix/kagane silently fails CORS preflight, parks in the retry queue, and drops at the cap. ## Verification 221 backend tests, 32 userscript tests, static `CGO_ENABLED=0` build, both compose configs. Two gaps, both real: 1. The userscript on live pages via Violentmonkey needs a human browser profile — not run. Check: comix series page (title/cover, no chapter), comix chapter page (records the number; an *older* chapter must not regress it), comix SPA navigation without reload, kagane series page (og:image cover), kagane reader (number from `og:title`), both chips opening the right sites. 2. The kagane browser path has not completed end-to-end anywhere. Dial/navigate/fetch is confirmed, but Cloudflare 403'd headless-shell's Chrome on every attempt from the dev sandbox, and comix's poll-through-Docker was blocked by that environment's TLS interception. Both environment-dependent rather than branch defects — the first real deploy is the actual verification.
sulthan added 16 commits 2026-08-03 19:32:46 +07:00
CLAUDE.md: swap Bromite for Violentmonkey throughout, add installed
golang skills to relevant skills, add comment-writing rules, add a
design-system pointer rule. docs/design-system.md: rewrite against
the current Claude Design project and the tokens/components already
shipped in backend/static/style.css (danger/slate/moss/clay/trash,
action key, brand mark).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
All Go files lived flat in backend/ as one package main. Move store,
latest-chapter polling, sessions, HTTP middleware, the JSON API, the
userscript handler, and the web UI (with its templates/static assets)
into backend/internal/{store,latest,session,httpmw,api,userscript,web},
each with an exported API. main.go becomes the composition root wiring
them into newRouter; root-level tests cover the assembled router while
package-local tests cover unit behavior. Update Dockerfile/.dockerignore
for the new internal/ tree and CLAUDE.md to describe the layout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extend the test harness's document stub with a querySelectorAll("img[alt]")
fake (module-level pageImages fixture, mirroring metaTags), then assert on
p.cover for a matching alt and for no match. Previously the guard in
coverFromPage() always short-circuited under test since querySelectorAll
didn't exist on the stub, so the alt-matching loop had zero coverage.
Also bumps backend/Dockerfile's build stage to golang:1.26-alpine —
chromedp v0.16.0 and cdproto both require go 1.26, and the pinned
1.24-alpine base no longer builds the module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Prod override put headless-shell on the externally-managed `proxy`
network so manga-api (confined there for Traefik routing) could still
resolve it. That reopened CDP (port 9222, raw remote code execution)
to every other container on that shared network, not just manga-api.

Give both services a project-private `browser` network (defined in
the base compose file, not `internal: true` since headless Chrome
needs outbound access to kagane.to). manga-api joins both `proxy` and
`browser` in the prod override; headless-shell never touches `proxy`.
Chained defects made the kagane browser-fetch path completely non-functional
in Docker Compose: headless-shell's compose command re-declared
--remote-debugging-port, colliding with the image's own entrypoint/socat
proxy (EOF on every dial); the sidecar was then only reachable by Docker DNS
name, which Chrome's DevTools HTTP handler rejects with a 500
(Host-header/DNS-rebinding check); and NewBrowserFetcher's NoModifyURL option
skipped /json/version discovery entirely, dialing a bare host:port that
Chrome 404s since /devtools/browser/<uuid> is minted fresh per Chrome start.
Fixed by trimming the redundant command flags, pinning headless-shell to a
static IP so BROWSER_WS_URL can name it directly, and removing NoModifyURL so
chromedp's discovery (which echoes the request's Host back into
webSocketDebuggerUrl) does the right thing on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- .env.example, DEPLOY.md, docker-compose.yml: add comix.to/kagane.to to
  ALLOWED_ORIGINS so the userscript isn't CORS-blocked on either new site
- .env.example: comment out BROWSER_WS_URL's DNS-name default, which
  overrides the working compose default and 500s Chrome's DevTools handler
- userscript: updateToCurrentChapter() now falls back to the stored
  chapter/label when chapterNum is unparseable, instead of wiping progress
  (kagane's og:title lacks a number when a chapter has no episode suffix)
- README.md: document comix/kagane in the config table, adapter reference,
  and key examples
sulthan added 1 commit 2026-08-03 19:53:09 +07:00
Captures what shipped on the branch:
- backend split into internal/ packages; composition root = main.go
- web UI go:embed now lives under internal/web/; Dockerfile must copy tree
- impeccable detector caveat (root-absolute /static/ paths) and false-clean
- confirm-row pattern for archive/finish/remove; --ember reserved
- edge-tab hitbox design (7x44 visible, 28x72 hit, touch-action + arm hold)
- Cinder design system section + ember-law reference
sulthan merged commit 180ee78b1f into main 2026-08-03 19:53:46 +07:00
Sign in to join this conversation.