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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "netfelix-audio-fix",
"version": "2026.04.21.12",
"version": "2026.04.21.13",
"scripts": {
"dev:server": "NODE_ENV=development bun --hot server/index.tsx",
"dev:client": "vite",
+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 });
+1 -5
View File
@@ -233,11 +233,7 @@ async function runScan(limit: number | null = null): Promise<void> {
setConfig("scan_running", "0");
log(`Scan complete: ${processed} scanned, ${errors} errors`);
emitSse("complete", { scanned: processed, total, errors });
if (getConfig("auto_processing") === "1") {
const { startProcessInbox } = await import("./review");
startProcessInbox();
}
// Auto-process loop (if enabled) picks up new inbox items automatically.
}
export default app;
+7 -6
View File
@@ -79,9 +79,9 @@ app.post("/audio-languages", async (c) => {
return c.json({ ok: true });
});
// Toggle the auto-processing flag. When flipped on, trigger a one-shot
// sort-inbox pass so existing Inbox items drain immediately without waiting
// for the next scan.
// Toggle the auto-processing flag. When flipped on, start a continuous
// polling loop that monitors the inbox and processes items as they arrive.
// When flipped off, stop the loop and abort any in-progress auto-run.
app.post("/auto-processing", async (c) => {
const body = await c.req.json<{ enabled?: unknown }>().catch(() => ({ enabled: null }));
if (typeof body.enabled !== "boolean") {
@@ -90,10 +90,11 @@ app.post("/auto-processing", async (c) => {
setConfig("auto_processing", body.enabled ? "1" : "0");
if (body.enabled) {
const { startProcessInbox } = await import("./review");
startProcessInbox();
const { startAutoProcessLoop } = await import("./review");
startAutoProcessLoop();
} else {
const { stopProcessInbox } = await import("./review");
const { stopAutoProcessLoop, stopProcessInbox } = await import("./review");
stopAutoProcessLoop();
stopProcessInbox();
}
return c.json({ ok: true, enabled: body.enabled });
+6 -1
View File
@@ -7,7 +7,7 @@ import pathsRoutes from "./api/paths";
import reviewRoutes from "./api/review";
import scanRoutes from "./api/scan";
import settingsRoutes from "./api/settings";
import { getDb } from "./db/index";
import { getConfig, getDb } from "./db/index";
import { log } from "./lib/log";
const app = new Hono();
@@ -66,6 +66,11 @@ log(`netfelix-audio-fix v${pkg.version} starting on http://localhost:${port}`);
getDb();
// Resume auto-process loop if it was enabled before the server restarted
if (getConfig("auto_processing") === "1") {
import("./api/review").then(({ startAutoProcessLoop }) => startAutoProcessLoop());
}
export default {
port,
fetch: app.fetch,