Phase 3: VISUAL feed and M-Files drawings in the worker
ci / frontend (push) Successful in 30s
ci / server (push) Successful in 4m12s
ci / image (push) Successful in 46s
ci / e2e (push) Successful in 1m24s

Migration 0004 (VISUAL and drawing caches, go-live ignored list, resource
map, job tables, order-derived columns with provenance; work order key
includes the operation sequence). The real worker on APScheduler with
settings re-read each fire, pause, run now, advisory locks, job_runs and a
heartbeat. The visual-feed job (five VisualKit reads, COPY and MERGE cache,
suspect-batch guard, the apply in one transaction: drafts, change orders,
retire-moved, work order gone, closed and cancelled observed, projects and
units from order links, auto-release) and the fingerprint. The drawing-lookup
job (queue, budget, download on version change, sha256 into assets, decision
table, staging for rev approval, drawing-origin change orders). Clerical From
VISUAL panel, flags, rev approval, hand uploads, change-order review; Admin
Feed settings and resource map. Synthetic fixture generator and a
fixture-driven worker in CI with a Playwright scenario. Docs updated.

Verified: 3249 server tests and 13 Playwright scenarios on the docker-1 CI
stack, 76 frontend tests, doctor at 0004; one feed and one drawing run
against VISUAL DEV and the vault from the developer machine (counts in the
changelog).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-10-16 14:45:38 -06:00
co-authored by Claude Fable 5.1
parent 16950055e3
commit 591fe90076
103 changed files with 16063 additions and 151 deletions
+18 -1
View File
@@ -13,8 +13,10 @@ import { dueInfo, flagClass, jobName, pillClass, statusLabel, targetHours } from
import { DrawingViewer } from "./DrawingViewer";
import { MoveSheet, SendToDeptSheet } from "./sheets";
import { DocumentsTab, HistoryTab, MaterialsTab, WorkOrdersTab, type Mutate } from "./tabs";
import { coSummary, coToReview } from "../lib/changeOrders";
import { ChangesTab, VisualBox } from "./visual";
type Tab = "drawing" | "materials" | "workorders" | "docs" | "history";
type Tab = "drawing" | "materials" | "workorders" | "changes" | "docs" | "history";
/** The rule's signature meaning for attaching a drawing by hand. */
const SIGN_ATTACH = "Attach a drawing by hand";
@@ -259,6 +261,7 @@ export function PacketPage() {
const fileIt = () => mutate((g) => packetsApi.transition(p.id, { action: "file", expect: { status: p.status, dept: p.dept.code }, ...g }), { sign: SIGN_FILE, ok: "Filed in the data book." });
const drawingPages = p.pages.filter((x) => x.type === "drawing");
const review = coToReview(p);
const shownTab: Tab = tab ?? (drawingPages.length ? "drawing" : "materials");
// The warehouse short-parts exception and the data book date are not edits of the packet itself.
const passive = new Set(["wh_receive_short", "set_databook_at"]);
@@ -276,6 +279,7 @@ export function PacketPage() {
{ id: "drawing", label: "Drawing", count: drawingPages.length },
{ id: "materials", label: "Materials", count: p.bom.length },
{ id: "workorders", label: "Work orders", count: p.work_orders.length },
{ id: "changes", label: "Changes", count: (p.change_orders ?? []).length },
{ id: "docs", label: "Documents", count: p.docs.length },
{ id: "history", label: "History" },
];
@@ -401,6 +405,18 @@ export function PacketPage() {
</div>
) : null}
{review && (me?.roles ?? []).some((r) => r === "clerical" || r === "clerical_supervisor" || r === "admin") ? (
<div className="callout warn" role="status">
<b>Change order {review.co_no}: a change from VISUAL is waiting for your approval</b> - {coSummary(review)}. Nothing has gone to the shop packet yet.
{review.hold ? <b> Shop packet on hold.</b> : null}{" "}
<button className="btn sm pri" type="button" onClick={() => setTab("changes")}>
Review change
</button>
</div>
) : null}
<VisualBox p={p} roles={me?.roles ?? []} sub={job} mutate={mutate} busy={busy} askReason={askReason} onError={(text) => setMsg({ ok: false, text })} />
{editing ? <HeaderEdit key={p.version} p={p} mutate={mutate} busy={busy} onDone={() => setEditing(false)} /> : null}
{hasDraftControls ? (
@@ -522,6 +538,7 @@ export function PacketPage() {
) : null}
{shownTab === "materials" ? <MaterialsTab p={p} mutate={mutate} busy={busy} /> : null}
{shownTab === "workorders" ? <WorkOrdersTab p={p} /> : null}
{shownTab === "changes" ? <ChangesTab p={p} roles={me?.roles ?? []} sub={job} mutate={mutate} busy={busy} askReason={askReason} /> : null}
{shownTab === "docs" ? <DocumentsTab p={p} mutate={mutate} busy={busy} /> : null}
{shownTab === "history" ? <HistoryTab p={p} /> : null}
</div>