From 2bb8c7df2476a6df66e8d2e7b679231905f69cad Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 22:33:16 +0000 Subject: [PATCH 1/2] chore: add CLAUDE.md, CI workflow, and Docker healthcheck for repo parity https://claude.ai/code/session_013yX3FPohZkQNiUGSNwwjbM --- .github/workflows/ci.yml | 23 +++++++++ CLAUDE.md | 101 +++++++++++++++++++++++++++++++++++++++ Dockerfile | 3 ++ 3 files changed, 127 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 CLAUDE.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bb0ed8e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run lint + - run: npm run typecheck + - run: npm run build + env: + SESSION_SECRET: ci_build_placeholder_secret_at_least_32_chars_long + NEXT_TELEMETRY_DISABLED: "1" diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f5b18cf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,101 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +npm run dev # http://localhost:7080 +npm run build +npm run typecheck +docker build -t rayderc/comicorbit:latest . +docker rm -f comicorbit; docker run -d --name comicorbit -p 7080:7080 \ + -v comicorbit_config:/config -v comicorbit_manga:/Manga \ + --restart unless-stopped rayderc/comicorbit:latest +``` + +## Architecture + +Hybrid Next.js router: `app/` (App Router, all UI) + `pages/api/` (Pages Router, all API routes). Auth is enforced server-side via `proxy.ts` (a Next.js middleware-style function) — it reads the iron-session cookie and redirects unauthenticated requests to `/login`. Routes excluded from auth: `/login`, `/setup`, all `/api/*`, `/opds/*`, and Next.js internals/static assets. + +### Startup tasks — `instrumentation.ts` + +`instrumentation.ts` runs once in the Node.js runtime on server start (skipped during edge/build). It performs, in order: + +1. **DB migration** (`lib/migration.ts`) — applies any pending schema changes. +2. **VAPID key generation** (`lib/webpush.ts` → `ensureVapidKeys`) — generates and persists VAPID keys in `site_config` if not already present. +3. **Page-count backfill** (`lib/migration.ts` → `backfillPageCounts`) — fills `chapters.page_count` for any chapters missing it (non-fatal, runs in background). +4. **Download worker** (`lib/downloader/index.ts` → `startWorker`) — processes queued downloads. +5. **Push outbox flusher** (`lib/notificationOutbox.ts` → `startOutboxFlusher`) — batches and sends push notifications. +6. **Series scan** (`lib/downloader/index.ts` → `scanAllSeries`) — scans for new chapters 10 s after boot, then repeats every 6 hours. + +### Auth — `lib/session.ts` + +iron-session, cookie name `comicorbit_session`. Production requires `SESSION_SECRET` ≥ 32 chars. `SESSION_COOKIE_SECURE` defaults to `false` (suitable for HTTP LAN installs); set to `true` behind a TLS-terminating proxy. Build phase uses a placeholder secret so `next build` succeeds without the env var. + +### CSRF — `lib/csrf.ts` + +Checks `Origin` header against `Host` on all mutating API routes. Requests without an `Origin` header pass (direct API clients, server-to-server). Exempted routes: `/api/setup`, `/api/login`. + +### Database — `lib/db.ts` + +Opens SQLite via `better-sqlite3`. Pragmas: `busy_timeout = 5000`, `journal_mode = WAL`, `foreign_keys = ON`. During `next build` (concurrent workers), uses an in-memory DB (`:memory:`) to avoid file contention. + +Key tables: + +| Table | Notes | +|---|---| +| `users` | `username` UNIQUE; `is_admin` flag; `anilist_token`; `legacy_password` for Werkzeug-hash migration | +| `series` | `slug` UNIQUE; `source` + `source_url` track origin; `one_shot`; `series_folder`; `reading_mode`; `anilist_id` | +| `series_tags` | Many-to-many tags, cascades on series delete | +| `chapters` | `UNIQUE(series_id, number)`; `page_count`; `file_path` to CBZ | +| `download_queue` | Status: `queued` / `downloading` / `done` / `error`; `progress_pct`; `current_chapter` | +| `read_progress` | `PRIMARY KEY (user_id, series_id, chapter_id)`; `page` + `completed` | +| `favorites` | `PRIMARY KEY (user_id, series_id)` | +| `push_subscriptions` | `UNIQUE(user_id, endpoint)`; stores Web Push `p256dh` + `auth` keys | +| `notification_outbox` | Batches per-chapter pushes; `PRIMARY KEY (user_id, series_id)`; flusher sends after idle window | +| `push_log` | Rolling diagnostics for push delivery attempts (status codes, errors) | +| `site_config` | Key/value store for VAPID keys and other runtime config | + +Inline migrations (idempotent `ALTER TABLE` / `CREATE TABLE IF NOT EXISTS`) run at startup via `lib/db.ts`. A separate `lib/migration.ts` handles heavier migrations (e.g. page-count backfill). + +### Downloader — `lib/downloader/` + +`lib/downloader/index.ts` exports `startWorker` (processes `download_queue`) and `scanAllSeries` (checks sources for new chapters and enqueues them). + +Sources (`lib/downloader/sources/`): + +- **MangaDex** (`mangadex.ts`) — uses the MangaDex REST API. +- **MangaFreak** (`mangafreak.ts`) — HTML scraper using `cheerio`. + +Sources implement the `Source` interface (`sources/types.ts`): `search`, `getSeriesMetadata`, `getChapterList`, `downloadChapter`. Multi-source search runs in parallel with a 15 s timeout per source. + +### OPDS — `app/opds/` + +OPDS 1.2 Atom feed for comic reader clients (Panels, Chunky, Paperback, etc.). Routes: `/opds` (root), `/opds/library`, `/opds/favorites`, `/opds/series/[id]`, `/opds/cbz/[chapterId]`. Auth uses HTTP Basic (username + password against the same `users` table) via `lib/basicAuth.ts` — not iron-session cookies. The proxy excludes `/opds/*` from cookie-auth. + +### Web Push — `lib/webpush.ts` / `lib/notificationOutbox.ts` + +VAPID keys generated once and stored in `site_config`. `push_subscriptions` table stores browser/device endpoints. The outbox batches multiple chapter notifications for the same series into one push message per user. + +## Design System + +Cyberpunk dark theme in `app/globals.css`, consistent with AstroFit/SkyBit. Key CSS custom properties: + +```css +--bg: #0a0a12 +--primary: #7c0eb3 +--accent-cyan: #22d3ee +--accent-magenta: #f472b6 +--font-mono: 'JetBrains Mono', 'Fira Code', 'SFMono-Regular', 'Consolas', monospace +``` + +Body uses system fonts (`-apple-system, Segoe UI, Roboto`). `var(--font-mono)` applies only to nav labels, badges, stat values, form labels, and the sidebar logo — never body text. Auth/modal cards use `overflow: hidden` with decorative bracket corners (cyan top-left, magenta bottom-right). Sidebar active state uses `box-shadow: inset 2px 0 0 var(--accent-cyan)`, not `border-left`. Page titles use chromatic `text-shadow`. No logo image — text-only branding. + +## Docker + +Port `7080` (both host and container). Two persistent volumes: `/config` (SQLite DB, VAPID keys, session data) and `/Manga` (downloaded CBZ files). WAL locking breaks on Windows bind mounts — use named volumes in production. + +The `build-certs/` directory at repo root is copied into the image so extra root CAs can be injected for builds behind TLS-intercepting proxies. An empty directory is fine for normal builds. + +`SESSION_COOKIE_SECURE=false` is set in the image default (and `docker-compose.yml`) for LAN HTTP installs. Set `SESSION_COOKIE_SECURE=true` in your compose file when running behind a TLS-terminating reverse proxy. diff --git a/Dockerfile b/Dockerfile index e7de59b..6c72356 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,4 +62,7 @@ RUN sed -i 's/\r$//' /usr/local/bin/docker-entrypoint.sh \ EXPOSE 7080 +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||'7080')+'/').then(r=>process.exit(r.status<500?0:1)).catch(()=>process.exit(1))" + ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] From 44a5e1aeef3d7edbd984cb347383b57ac25a842c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 22:38:50 +0000 Subject: [PATCH 2/2] fix: use 'eslint .' lint script and fix prefer-const so CI lint passes next lint was removed in Next 16; switch to eslint directly. Fix a prefer-const error in mangafreak.ts surfaced by the now-working lint. --- lib/downloader/sources/mangafreak.ts | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/downloader/sources/mangafreak.ts b/lib/downloader/sources/mangafreak.ts index 501dd64..b52add3 100644 --- a/lib/downloader/sources/mangafreak.ts +++ b/lib/downloader/sources/mangafreak.ts @@ -160,7 +160,7 @@ export const mangafreakSource: Source = { }, async fetchChapter(ref: ChapterRef, onProgress: ProgressFn, signal): Promise { - let urls: string[] = []; + const urls: string[] = []; // officialPageCount comes from MangaFreak's own page-selector dropdown. // It reflects only real manga pages — promotional images beyond this count are excluded. let officialPageCount = 0; diff --git a/package-lock.json b/package-lock.json index 2d733fb..a0d9745 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "comicorbit", - "version": "0.2.0", + "version": "0.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "comicorbit", - "version": "0.2.0", + "version": "0.3.4", "dependencies": { "archiver": "^7.0.1", "bcryptjs": "^3.0.3", diff --git a/package.json b/package.json index 1e43783..1de1003 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dev": "next dev -p 7080", "build": "next build", "start": "next start -p 7080", - "lint": "next lint", + "lint": "eslint .", "typecheck": "tsc --noEmit" }, "dependencies": {