Files
MangaCircus/tools/snapshot-stopped.js
T
MaashkinoozheandClaude Opus 5.5 b2bb34174c Fold the sentinel into the scraper and drop the sentinel service (BL-0046)
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>
2026-10-22 22:58:03 +02:00

29 lines
2.1 KiB
JavaScript

#!/usr/bin/env node
'use strict';
// Targeted recovery snapshot, not a library backup. Caller must prove all writers
// are stopped first. Never run this concurrently with the reader or scraper.
const fs = require('node:fs'), path = require('node:path'), crypto = require('node:crypto');
if (process.argv[2] !== '--writers-stopped') throw new Error('First verify that every writer is stopped, then pass --writers-stopped');
const root = path.resolve(__dirname, '..'), library = path.join(root, 'library'), data = path.join(root, 'data');
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
const backup = path.join(data, 'scraper', 'backups', stamp + '-compose-cutover');
fs.mkdirSync(backup, { recursive: true });
const files = [];
function copy(source, relative) {
if (!fs.existsSync(source)) return;
if (fs.statSync(source).isDirectory()) {
for (const name of fs.readdirSync(source)) copy(path.join(source, name), path.join(relative, name));
return;
}
const target = path.join(backup, relative); fs.mkdirSync(path.dirname(target), { recursive: true });
fs.copyFileSync(source, target);
files.push({ path: relative, bytes: fs.statSync(source).size, sha256: crypto.createHash('sha256').update(fs.readFileSync(source)).digest('hex') });
}
for (const name of ['db.json', 'scan-cache.json', 'catalog.json', 'collection-covers', 'runtime-reader.json']) copy(path.join(data, name), path.join('data', name));
for (const name of ['queue.json', 'runtime.json', 'daemon.lock', 'supervisor.lock']) copy(path.join(data, 'scraper', name), path.join('data', 'scraper', name));
for (const name of fs.readdirSync(library)) {
copy(path.join(library, name, name.startsWith('.mcscraper-work-') ? 'job.json' : 'manga.json'), path.join('library', name, name.startsWith('.mcscraper-work-') ? 'job.json' : 'manga.json'));
}
fs.writeFileSync(path.join(backup, 'snapshot.json'), JSON.stringify({ created: new Date().toISOString(), kind: 'targeted-state-not-image-backup', files }, null, 2));
console.log(JSON.stringify({ backup, files: files.length, imageFilesCopied: false }));