MEGAINFRA-WEBAPP/scripts/dn-board.mts
DEls 1031a103c0 chore(dn): add two read-only checks for the staged rollout
dn-snapshot prints row counts across the delivery-note collections. The
two that matter are delivery_notes and delivery_note_items: the staged
pipeline must never move them on its own, so a reading before and after a
test says plainly whether anything reached the ledger.

dn-board renders the inbox board as text, which is how the pipeline can
be read over SSH or while the app is mid-deploy.

Both are read-only. The other scripts written while diagnosing this work
were one-off probes and are not kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 01:00:44 +02:00

62 lines
2.8 KiB
TypeScript

// The delivery-note inbox board, as text. Read-only.
//
// Renders what /dashboard/financial/delivery-notes/inbox would show, so
// the pipeline can be checked over SSH or while the app is mid-deploy.
// Batches hold the left-hand columns; once assembly is settled the batch
// dissolves and its documents continue individually.
//
// npx tsx --env-file=.env.local scripts/dn-board.mts
//
// .mts, not .ts: top-level await, and the package has no "type": "module".
import { pbAdmin } from "../src/lib/pb";
import { displayBatchStage, displayCandidateStage } from "../src/lib/dn-pipeline/stages";
type Row = Record<string, any>;
const COLUMNS: { key: string; label: string }[] = [
{ key: "queued", label: "Queued" },
{ key: "reading", label: "Reading photos" },
{ key: "split_review", label: "Check page grouping" },
{ key: "needs_review", label: "Needs matching" },
{ key: "ready_to_post", label: "Ready to post" },
{ key: "posted", label: "Complete" },
];
const pb = await pbAdmin();
const batches = (await pb.collection("dn_batches").getFullList()) as Row[];
const notes = (await pb.collection("dn_batch_notes").getFullList({ expand: "supplier" })) as Row[];
const batchById = new Map(batches.map((batch) => [batch.id, batch]));
const columns: Record<string, string[]> = Object.fromEntries(COLUMNS.map((column) => [column.key, []]));
for (const batch of batches) {
const stage = displayBatchStage(batch.status ?? "", batch.stage ?? "");
if (stage === "failed") continue;
if (columns[stage] && stage !== "complete") {
columns[stage].push(`BATCH ${batch.id} · ${batch.source_count ?? 0} source(s) · ${batch.page_count ?? 0} page(s)`);
}
}
for (const note of notes) {
const batch = batchById.get(note.batch);
if (!batch || displayBatchStage(batch.status ?? "", batch.stage ?? "") !== "complete") continue;
const stage = displayCandidateStage(note.status ?? "", note.stage ?? "");
// `matching` is the runner's transient state; to an operator it is the
// same outstanding job as needs_review.
const key = stage === "matching" ? "needs_review" : stage;
if (!columns[key]) continue;
const supplier = note.expand?.supplier?.name ?? note.supplier_name_raw ?? "supplier unknown";
columns[key].push(`${note.doc_number || "(unnumbered)"} — ${supplier}`);
}
for (const { key, label } of COLUMNS) {
console.log(`\n${label} (${columns[key].length})`);
for (const line of columns[key]) console.log(` • ${line}`);
if (columns[key].length === 0) console.log(" —");
}
const failed = batches.filter((batch) => displayBatchStage(batch.status ?? "", batch.stage ?? "") === "failed");
if (failed.length > 0) {
console.log(`\nFailed (${failed.length})`);
for (const batch of failed) console.log(` • ${batch.id} — ${batch.last_error || "no error recorded"}`);
}