make auto-process inbox a continuous polling loop instead of one-shot
Build and Push Docker Image / build (push) Successful in 59s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 11:38:58 +02:00
parent 39fcac10b5
commit b738f6878d
5 changed files with 80 additions and 24 deletions
+65 -11
View File
@@ -1101,9 +1101,8 @@ app.post("/approve-batch", async (c) => {
let processInboxAbort: AbortController | null = null;
/**
* Single entry point for launching processInbox. Manages the abort controller
* so every caller (manual button, auto-process toggle, post-scan auto-process)
* can be stopped via the /process-inbox/stop endpoint.
* One-shot trigger for the manual "Process Inbox →" button. Manages the abort
* controller so in-progress runs can be stopped via /process-inbox/stop.
* Returns false if a run is already in progress.
*/
export function startProcessInbox(): boolean {
@@ -1134,6 +1133,67 @@ export function stopProcessInbox(): boolean {
return false;
}
// ─── Auto-process loop ─────────────────────────────────────────────────────
// Continuous polling loop: while enabled, checks every few seconds for
// unsorted inbox items and processes them. The manual button is a one-shot;
// this is the "always on" counterpart.
const AUTO_PROCESS_POLL_MS = 5_000;
let autoProcessTimer: Timer | null = null;
function scheduleAutoProcessTick() {
if (autoProcessTimer) return;
if (getConfig("auto_processing") !== "1") return;
autoProcessTimer = setTimeout(autoProcessTick, AUTO_PROCESS_POLL_MS);
}
async function autoProcessTick() {
autoProcessTimer = null;
if (getConfig("auto_processing") !== "1") return;
// Don't overlap with a manual run
if (processInboxAbort) {
scheduleAutoProcessTick();
return;
}
const db = getDb();
const { n } = db
.prepare("SELECT COUNT(*) as n FROM review_plans WHERE status = 'pending' AND is_noop = 0 AND sorted = 0")
.get() as { n: number };
if (n > 0) {
processInboxAbort = new AbortController();
const { signal } = processInboxAbort;
try {
const result = await processInbox(db, getAudioLanguages(), undefined, {
onStart: emitInboxSortStart,
onProgress: emitInboxSortProgress,
signal,
});
emitInboxSorted(result);
} catch {
emitInboxSorted({ moved_to_queue: 0, moved_to_review: 0 });
} finally {
processInboxAbort = null;
}
}
scheduleAutoProcessTick();
}
export function startAutoProcessLoop() {
scheduleAutoProcessTick();
}
export function stopAutoProcessLoop() {
if (autoProcessTimer) {
clearTimeout(autoProcessTimer);
autoProcessTimer = null;
}
}
app.post("/process-inbox", async (c) => {
if (!startProcessInbox()) {
return c.json({ ok: false, error: "processing already running" }, 409);
@@ -1534,10 +1594,7 @@ app.post("/:id/rescan", async (c) => {
// Delete pending jobs
db.prepare("DELETE FROM jobs WHERE item_id = ? AND status = 'pending'").run(id);
// Auto-process if enabled (processInbox handles language resolution + reanalysis)
if (getConfig("auto_processing") === "1") {
await processInbox(db, getAudioLanguages());
}
// Auto-process loop (if enabled) picks up the reset item automatically.
emitPipelineChanged();
return c.json({ ok: true, inInbox: true });
@@ -1571,10 +1628,7 @@ app.post("/rescan-series", async (c) => {
db.prepare("DELETE FROM jobs WHERE item_id = ? AND status = 'pending'").run(item.id);
}
// Auto-process if enabled
if (getConfig("auto_processing") === "1") {
await processInbox(db, getAudioLanguages());
}
// Auto-process loop (if enabled) picks up reset items automatically.
emitPipelineChanged();
return c.json({ ok: true, count: items.length });