A reader tab left open on one device (or restored by the browser) used to
save its old chapter as the newest position as soon as it was shown again.
Progress saves now carry the tab id and the newest progress time the tab
knows (`seen`); the server refuses a save when another tab or device saved
a newer position since. The reader then asks: jump to the newest position,
or keep reading here (which makes this chapter the head again). It checks on
open, when the tab becomes visible, and on every save. Reader links carry
`at` and the reader keeps it in the address bar, so reloads and restored
tabs still know what they had seen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each sign-in keeps its own reader mode, zoom, page tone and skipping once
it changes one; sessions that never changed follow the latest set, and a
new sign-in starts from the session used last. Settings -> Reading shows
every session's state, lets a session follow the latest again or become
the latest, and turns the feature off for one shared set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reader polled /api/state every 15 s; each call rebuilt the scraper's library
summary when it was over 60 s old, reading every chapter.json (on the NAS about
5,300 opens / 16,000 NFS calls and 1.3 s CPU per walk, once a minute, plus 115 KB
per poll). The reader now polls GET /api/revision (falls back to /api/state on an
older scraper), and the summary is rebuilt after a library change or after 15
minutes. Release-date writes invalidate it too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each user gets an own read-ahead queue, served in turn, so two people reading
at once no longer cancel each other's chapter. The report rounds phase times
and measures the quiet time before a slow storage probe correctly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A switchable trace (DATA/perf/trace.on or PERF_TRACE=1) records every request's
phases (stat, open, first read, disk vs network wait), browser resource timings,
page-turn waits and flow stalls, a cold storage probe, event-loop lag, scans and
scraper calls as JSONL. tools/perf-report.js says which phase slow loads spend
their time in; tools/perf-bench.js simulates readers.
READ_AHEAD (default on) reads an opened chapter's pages, then the next chapter's,
into the OS cache so a NAS's cold reads stop reaching the browser.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f38f218 failed to build: .dockerignore is an allowlist and kept
moments.js out of the build context. Verified by building the image and
starting the reader from it (health ok).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 5497e8b image lacked moments.js, so the reader crashed at start on
docker-01. test/dockerfile.test.js follows server.js requires and fails
when a top-level module is not copied into the image.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The save sheet gets "+ Previous page" / "+ Next page" (right to left for
rtl in Page/Book) for a picture that runs over several pages. Moments
store a layout (row | column); pages the selected region does not touch
are dropped. Crops, viewer and gallery draw both layouts.
Dialogs now clean up directly instead of waiting for the "close" event,
which a backgrounded page can deliver late (a closed sheet stayed in the
page and M stopped working).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reader: bookmark button and M save the page (Flow: page at the reading
line, Book: the spread) with an optional dragged region, exactly one
category (Twist, Hot, Awesome, Cute, Funny, Feels) and a note. I now
toggles the bars; ? lists all keys. Keys dispatch through public/keys.js,
which also feeds the new Settings -> Keyboard shortcuts section.
Manga page: "Your moments" strip of crops (story or recent order,
category filters), viewer with step, full page, edit, delete and
"Open in reader" (read.html ...&p=). Moments point at pages by file
name and show "page moved" when the file is gone. Private per user.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row now sits below the search, filters and tags, directly above the
grid. It hides while a search is typed or a status filter is set (as well
as on "Recently read"); tags and collection grouping keep it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One fixed row above the library showing the five most recently read,
unfinished manga with the normal tiles (they stay in the grid as well).
It ignores search and filters, hides while sorted by "Recently read",
and can be turned off under Settings > Reading > Library page
(continueRow, default on).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page hotlinked ~100 AniList CDN covers per load and some failed at random.
The reader now downloads each catalog cover once (sequential, ~1/s, stops on
401/403/429, images up to 2 MB) into data/greatest-covers/ and serves it from
/api/greatest-cover with a year-long browser cache; until a copy exists the
route redirects to the catalog URL. The page also accepts the server's
relative cover URLs, so owned titles show their library cover again.
GREATEST_COVER_WARM=0 turns the downloads off (tests, offline installs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Cache prune counts directory entries first and only reads files when over
the cap; tests for cross-kind prune, schema-mismatch and corrupt entries.
- Scoring tolerates malformed manga.json and cached records (non-list tags,
object or null relations, non-object items); a tag without a rank no longer
passes the top-tag filter; authors without an integer AniList id are dropped.
- discovery.json publishes only candidates some feed shows; examined/clipped
still describe the pool. Tests that asserted the published count equals the
pool size now check the pool via candidatePool() and the published set
against the feeds.
- The post-harvest rescore failure test now breaks discovery.json right before
the publish (via a scraper stand-in) instead of relying on tags: 5, and a
following successful harvest clears the stale lastError.
- Consent text names everything sent to AniList; paused label mentions admin
harvests; the UI test asserts the XSS seed instead of skipping it.
- Slot-mix test proves the fresh/outside reservations matter.
- Docs: privacy note, fresh fallback reason, per-period cap, dry-run wording,
published-candidate rule; plan/spec hygiene and spec status.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both are unofficial sites that mostly host unlicensed copies; MangaDex (official
API) stays on. Settings → Admin → Scraper → Hoster settings has a 'Use <source>'
switch. A switched-off source refuses new jobs, keeps queued jobs waiting and is
skipped by nightly update checks. Installations whose queue or history already
used a source keep it switched on, so upgrades don't stop a working queue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- docs/deployment.md: step-by-step Docker deployment for anyone (setup,
configuration, LAN/HTTPS/VPN access, adding manga, operation, prebuilt
images, backups, peering, troubleshooting, rules for agents).
- deploy/circus.sh: host helper without Node (status, up/down with queue
pause, logs, pause/resume, update, rescan, restore-test); deploy/compose.https.yml
+ Caddyfile for automatic HTTPS.
- README rewritten for public readers (Docker first, sources/copyright note,
docs index); AGENTS.md + CLAUDE.md for contributors and coding agents.
- Removed notes about one particular installation (Windows host, handoffs,
audits of a private library, benchmarks with local paths, wishlist, reading
map) and the unused ops/proxmox templates; generalized remaining mentions of
hosts, VPN and Windows-only commands.
- Defaults: TZ and reading time zone UTC; .gitattributes keeps shell/YAML LF.
- Personal agent coordination files are git-ignored (CLAUDE.local.md,
.claude/skills/manga-org/).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- scraper/metadata/release-dates.js fills x-dates.release from MangaUpdates
publisher releases. MangaUpdates also lists English back-catalog re-releases,
so a date is kept only inside the AniList original run, when no scanlation or
source upload is more than 4 days earlier, and when it fits the release order
(no lower chapter dated after a higher one). Seasons that restart chapter
numbers are skipped; dates this job wrote and no longer accepts are withdrawn.
- The scraper refreshes ongoing series nightly before its update checks, only
while no download runs, and bumps the publish revision so the reader reconciles.
- tools/release-dates.js for a whole library (dry run default). Production dry
run: 728 chapters dated (Kagurabachi, JJK, Chainsaw Man, One Piece from 928).
- Peer sync: chapters this library already has adopt a peer's earlier ecosystem
date (job result ecosystemLowered); the peer listing now carries dates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Chapter rows show when you first finished a chapter (setting: date + hour,
date, off) from a new durable progress doneAt; older reads fall back to the
reading clock's earliest remembered completion.
- chapter.json keeps added (arrival in this library, now kept on replace) plus
x-dates.source (source upload, bulk imports flagged), x-dates.release
(official, for a later metadata job) and x-dates.ecosystem (first entry into
any Manga Circus library; carried over peering, earliest wins, clamped).
- Rows show one release date by tier: official > source upload > ecosystem >
library, with all known dates in the tooltip (setting to hide).
- Fixes: WeebCentral dates came from the chapter page, which has none (every
chapter got the download time); they now come from the full chapter list.
MangaDex no longer writes a group's publishAt into added. Peering sends dates.
- tools/backfill-chapter-dates.js migrates an existing library (dry run default).
- Coverage dry run on the server: docs/chapter-dates-coverage-2026-10-24.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stall watchdog now runs inside the scraper (ScraperService.checkStalls,
every 30 s from the work loop): same rule as before, one checkpoint-safe
restart, quarantine on the second stall, missing checkpoint is no evidence,
and paused time no longer counts. Stalls are recorded as scraper incidents
(failure.kind 'stall'). The private /api/recovery endpoint is gone.
Dropped with the service: reader/scraper-unreachable incidents (supervisor
and Docker handle crashes; the health page shows "Scraper: unreachable"),
duplicate job-failure incidents and the unused /metrics endpoint.
The stack is now reader + scraper (+ optional peer-gateway).
The supervisor test moved to runtime/test (npm run test:runtime stays).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An uncached scan probed one image at a time and read 256 KB of each. On
a NAS that is bound by per-file latency (the server/Synology: ~20 ms per
image, ~100 min for 298k images). Now chapter folders of a book and
their images are probed SCAN_CONCURRENCY (default 8, 1-64) at a time
and assembled in folder order, and imageSize() reads 32 KB first, then
256 KB, then the whole file: a prefix gives either no size or the same
size, so results do not change. Tests: every format and read stage
equals the whole-file answer; concurrency 1 and 8 build the same index
and cache; the limiter keeps its bound and per-task errors. (BL-0043)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New POST /api/admin/user/role {name, admin}: admin-only (checked on the
server), keeps the password and sessions, takes effect on the target's
current session at once, logs who changed whom. Revoking your own
rights is refused, so at least one admin always remains. Settings ->
Users & access shows "Make admin" / "Remove admin" next to every other
user, with a confirmation. Test: normal users and anonymous callers are
refused, input checks, promotion and revocation apply immediately and
are saved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the server the backup folder is a Synology NFS share with "map all
users to admin": access(W_OK) returns EACCES for uid 1000 although
writes succeed, so the first scheduled run failed with "not usable".
Create and remove a probe file instead; test that it leaves nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reader owns a scheduled backup into BACKUP (Compose: BACKUP_PATH mounted at
/backup, off when unset). A run pauses the scraper queue through its control
API, waits (bounded) until no job is active, writes a verified tar.gz of DATA
with db.json from memory and scraper/queue.json stored paused, excludes
runtime/lock/pid/temp files, resumes only a queue it paused, then applies
count- or day-based retention to its own archive names only. Schedule,
retention, "Back up now" and last result live in Settings > Service health.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zero-dependency Node server + plain HTML/JS frontend.
- Library is a folder tree (Manga/Book/Chapter/pages), indexed in place;
page roles (cover, double, extra, rotation) are file name tags
- Accounts with per-user progress, reading settings, follows and
new-chapter notifications (in-app + optional webhook)
- Reader with Flow, Page and Book modes, pointer-anchored page zoom,
tap zones, auto-hiding controls, chapter picker, page preloading
- Multiple versions per chapter with community votes
- Docker image and compose file
- Demo library generator (tools/make-dummy.js)
- Docs: architecture, library format, import, scraper guide