fix inbox sort during scan, move dropdown to button row, per-item Process button
Build and Push Docker Image / build (push) Successful in 48s

- sort state lifted to PipelinePage so loadGroups includes the sort param
  on every reload (scan SSE events no longer reset the sort)
- sort dropdown moved from subtitle to ColumnShell middle slot (left of
  Process Inbox button)
- ColumnShell.skip renamed to middle, accepts ReactNode or ColumnAction
- per-item "Process →" button on inbox movie cards and series cards:
  POST /:id/process resolves language + reanalyzes + sorts a single item
- dashboard stat pills refresh during scan (every 25 items)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 09:18:52 +02:00
parent 6325bdb3e9
commit 789a9f7bfe
10 changed files with 1576 additions and 60 deletions
+59
View File
@@ -1123,6 +1123,65 @@ app.post("/process-inbox/stop", (c) => {
return c.json({ ok: true, message: "not running" });
});
// ─── Process single item ────────────────────────────────────────────────────
// Runs language resolution + reanalysis + sort for one inbox item.
app.post("/:id/process", async (c) => {
const db = getDb();
const id = parseId(c.req.param("id"));
if (id == null) return c.json({ error: "invalid id" }, 400);
const plan = db
.prepare("SELECT id FROM review_plans WHERE item_id = ? AND status = 'pending' AND sorted = 0")
.get(id) as { id: number } | undefined;
if (!plan) return c.json({ error: "item not in inbox" }, 404);
// Build language resolver (same as processInbox)
const cfg = getAllConfig();
const radarrCfg = { url: cfg.radarr_url, apiKey: cfg.radarr_api_key };
const sonarrCfg = { url: cfg.sonarr_url, apiKey: cfg.sonarr_api_key };
const radarrEnabled = cfg.radarr_enabled === "1" && radarrUsable(radarrCfg);
const sonarrEnabled = cfg.sonarr_enabled === "1" && sonarrUsable(sonarrCfg);
const [radarrLibrary, sonarrLibrary] = await Promise.all([
radarrEnabled ? loadRadarrLibrary(radarrCfg) : Promise.resolve(null),
sonarrEnabled ? loadSonarrLibrary(sonarrCfg) : Promise.resolve(null),
]);
const resolverCfg: LanguageResolverConfig = {
radarr: radarrEnabled ? radarrCfg : null,
sonarr: sonarrEnabled ? sonarrCfg : null,
radarrLibrary,
sonarrLibrary,
};
// Resolve language
const langResult = await resolveLanguage(db, id, resolverCfg);
if (langResult.externalRaw != null) {
db
.prepare("UPDATE media_items SET original_language = ?, orig_lang_source = ?, needs_review = ? WHERE id = ?")
.run(langResult.origLang, langResult.origLangSource, langResult.needsReview, id);
}
// Reanalyze + sort
const audioLanguages = getAudioLanguages();
reanalyze(db, id, audioLanguages);
const updated = db.prepare("SELECT auto_class, is_noop FROM review_plans WHERE item_id = ?").get(id) as
| { auto_class: string | null; is_noop: number }
| undefined;
if (updated && !updated.is_noop) {
if (updated.auto_class === "auto") {
db
.prepare("UPDATE review_plans SET status = 'approved', reviewed_at = datetime('now'), sorted = 1 WHERE id = ?")
.run(plan.id);
const { item, streams, decisions } = loadItemDetail(db, id);
if (item) enqueueAudioJob(db, id, buildCommand(item, streams, decisions));
} else {
db.prepare("UPDATE review_plans SET sorted = 1 WHERE id = ?").run(plan.id);
}
}
emitPipelineChanged();
return c.json({ ok: true, destination: updated?.auto_class === "auto" ? "queue" : "review" });
});
// ─── Approve all ready ───────────────────────────────────────────────────────
// Bulk-approves every auto_heuristic-classified plan currently in Review.
app.post("/approve-ready", (c) => {