0fd3624d9f
Build and Push Docker Image / build (push) Successful in 4m3s
column headers are now a fixed three-row layout (title / subtitle / button row). every column always reserves all three rows so headers line up regardless of contents; actions render disabled when their column is empty instead of disappearing, which keeps the header height stable as state changes. the processing column gets a new "Auto-process Queue" checkbox that mirrors the inbox's "Auto-process Inbox" toggle. backend adds an auto_process_queue config, a maybeStartQueueProcessor() helper, a POST /api/settings/auto-process-queue endpoint, and a hook in enqueueAudioJob so approvals drain the queue hands-off when the toggle is on. reopen-all and per-item reopen now send items to the Inbox (sorted=0) instead of back to Review. the done column's label and tooltip become "← Back to inbox" to match, and the clear button moves to the right slot so the header pattern (left=backward, right=forward) stays consistent across columns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
import { Database } from "bun:sqlite";
|
|
import { describe, expect, test } from "bun:test";
|
|
import { SCHEMA } from "../../db/schema";
|
|
import { reopenAllDone, unsortAll } from "../review";
|
|
|
|
function makeDb(): Database {
|
|
const db = new Database(":memory:");
|
|
for (const stmt of SCHEMA.split(";")) {
|
|
const trimmed = stmt.trim();
|
|
if (trimmed) db.run(trimmed);
|
|
}
|
|
return db;
|
|
}
|
|
|
|
function seedPlan(db: Database, id: number, opts: { sorted?: 0 | 1; status?: string; isNoop?: 0 | 1 } = {}) {
|
|
const { sorted = 1, status = "pending", isNoop = 0 } = opts;
|
|
db
|
|
.prepare(
|
|
"INSERT INTO media_items (id, jellyfin_id, type, name, file_path, container) VALUES (?, ?, 'Movie', ?, ?, 'mkv')",
|
|
)
|
|
.run(id, `jf-${id}`, `Item ${id}`, `/x/${id}.mkv`);
|
|
db
|
|
.prepare(
|
|
"INSERT INTO review_plans (item_id, status, is_noop, auto_class, sorted, apple_compat, job_type) VALUES (?, ?, ?, 'auto_heuristic', ?, 'direct_play', 'copy')",
|
|
)
|
|
.run(id, status, isNoop, sorted);
|
|
}
|
|
|
|
describe("unsortAll", () => {
|
|
test("flips sorted=1 pending plans back to sorted=0, skips is_noop and non-pending", () => {
|
|
const db = makeDb();
|
|
seedPlan(db, 1, { sorted: 1, status: "pending" });
|
|
seedPlan(db, 2, { sorted: 1, status: "pending" });
|
|
seedPlan(db, 3, { sorted: 0, status: "pending" }); // already in inbox
|
|
seedPlan(db, 4, { sorted: 1, status: "approved" }); // queued
|
|
seedPlan(db, 5, { sorted: 1, status: "pending", isNoop: 1 }); // noop
|
|
|
|
const count = unsortAll(db);
|
|
expect(count).toBe(2);
|
|
|
|
const rows = db.prepare("SELECT item_id, sorted, status FROM review_plans ORDER BY item_id").all() as {
|
|
item_id: number;
|
|
sorted: number;
|
|
status: string;
|
|
}[];
|
|
expect(rows).toEqual([
|
|
{ item_id: 1, sorted: 0, status: "pending" },
|
|
{ item_id: 2, sorted: 0, status: "pending" },
|
|
{ item_id: 3, sorted: 0, status: "pending" },
|
|
{ item_id: 4, sorted: 1, status: "approved" },
|
|
{ item_id: 5, sorted: 1, status: "pending" },
|
|
]);
|
|
});
|
|
|
|
test("noop when review column is empty", () => {
|
|
const db = makeDb();
|
|
expect(unsortAll(db)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("reopenAllDone", () => {
|
|
test("flips done/error plans back to pending + inbox (sorted=0) and drops their jobs", () => {
|
|
const db = makeDb();
|
|
seedPlan(db, 1, { status: "done" });
|
|
seedPlan(db, 2, { status: "error" });
|
|
seedPlan(db, 3, { status: "approved" }); // untouched
|
|
db.prepare("INSERT INTO jobs (item_id, command, job_type, status) VALUES (1, 'ffmpeg', 'copy', 'done')").run();
|
|
db.prepare("INSERT INTO jobs (item_id, command, job_type, status) VALUES (2, 'ffmpeg', 'copy', 'error')").run();
|
|
db.prepare("INSERT INTO jobs (item_id, command, job_type, status) VALUES (3, 'ffmpeg', 'copy', 'pending')").run();
|
|
|
|
const count = reopenAllDone(db);
|
|
expect(count).toBe(2);
|
|
|
|
const statuses = db
|
|
.prepare("SELECT item_id, status, sorted, reviewed_at FROM review_plans ORDER BY item_id")
|
|
.all() as {
|
|
item_id: number;
|
|
status: string;
|
|
sorted: number;
|
|
reviewed_at: string | null;
|
|
}[];
|
|
expect(statuses[0]).toMatchObject({ status: "pending", sorted: 0, reviewed_at: null });
|
|
expect(statuses[1]).toMatchObject({ status: "pending", sorted: 0, reviewed_at: null });
|
|
// Untouched plan keeps its pre-existing sorted=1 (default for the seed).
|
|
expect(statuses[2]).toMatchObject({ status: "approved", sorted: 1 });
|
|
|
|
const jobs = db.prepare("SELECT item_id, status FROM jobs ORDER BY item_id").all();
|
|
expect(jobs).toEqual([{ item_id: 3, status: "pending" }]);
|
|
});
|
|
|
|
test("noop when nothing is done or errored", () => {
|
|
const db = makeDb();
|
|
seedPlan(db, 1, { status: "pending" });
|
|
expect(reopenAllDone(db)).toBe(0);
|
|
});
|
|
});
|