Phase 3: VISUAL feed and M-Files drawings in the worker
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:
@@ -40,6 +40,8 @@ export function Card({ p, extra }: { p: PacketCard; extra?: ReactNode }) {
|
||||
{cos ? <span className="pill st-review">CO {cos}</span> : null}
|
||||
</div>
|
||||
{p.flag ? <div className={`small ${flagClass(p.flag)}`}>⚑ {p.flag}</div> : null}
|
||||
{p.co_pending ? <div className="small due soon">Change from VISUAL to approve</div> : null}
|
||||
{p.visual_closed && p.status !== "filed" ? <div className="small muted">Closed in VISUAL</div> : null}
|
||||
{extra}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FLAG_FILTERS } from "../lib/packets";
|
||||
import { PickUnit, type UnitPick } from "./PickUnit";
|
||||
|
||||
export type BoardView = "board" | "list";
|
||||
@@ -11,10 +12,13 @@ interface FindBarProps {
|
||||
onView: (v: BoardView) => void;
|
||||
/** "Showing n packets". */
|
||||
count: number | null;
|
||||
/** The flag kind filter. Shown when onFlag is given. */
|
||||
flag?: string;
|
||||
onFlag?: (v: string) => void;
|
||||
}
|
||||
|
||||
/** Find a job: the search box, the project then unit picker, and the Board | List toggle. */
|
||||
export function FindBar({ q, onQ, pick, onPick, view, onView, count }: FindBarProps) {
|
||||
export function FindBar({ q, onQ, pick, onPick, view, onView, count, flag = "", onFlag }: FindBarProps) {
|
||||
return (
|
||||
<section className="fltbar" aria-label="Find jobs">
|
||||
<div className="fltrow">
|
||||
@@ -39,6 +43,18 @@ export function FindBar({ q, onQ, pick, onPick, view, onView, count }: FindBarPr
|
||||
<div className="f fltp">
|
||||
<PickUnit value={pick} onChange={onPick} />
|
||||
</div>
|
||||
{onFlag ? (
|
||||
<label className="f">
|
||||
<span>Flags</span>
|
||||
<select className="in" value={flag} onChange={(e) => onFlag(e.target.value)}>
|
||||
{FLAG_FILTERS.map(([v, l]) => (
|
||||
<option key={v} value={v}>
|
||||
{l}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
) : null}
|
||||
<div className="fltbtns">
|
||||
<div className="seg" role="group" aria-label="View">
|
||||
<button type="button" aria-pressed={view === "board"} onClick={() => onView("board")}>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { JobRun } from "../api/types";
|
||||
import { count, runPillClass, RUN_STATUS_LABEL, TRIGGER_LABEL } from "../lib/feed";
|
||||
import { fmtTime } from "../lib/format";
|
||||
|
||||
/** The last runs of the VISUAL feed as a small table. `attached` maps a run id to the drawings attached after it. */
|
||||
export function RunsTable({ runs, attached, label = "Last runs" }: { runs: JobRun[]; attached?: Map<number, number>; label?: string }) {
|
||||
if (!runs.length) return <div className="empty slim">No runs yet.</div>;
|
||||
return (
|
||||
<div className="tw">
|
||||
<table aria-label={label}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>When</th>
|
||||
<th>Trigger</th>
|
||||
<th>Status</th>
|
||||
<th className="num">New</th>
|
||||
<th className="num">Updated</th>
|
||||
<th className="num">Change orders</th>
|
||||
<th className="num">Drawings attached</th>
|
||||
<th>Errors</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{runs.map((r) => (
|
||||
<tr key={r.id}>
|
||||
<td className="small mono nowrap">{fmtTime(r.started_at)}</td>
|
||||
<td className="small">{TRIGGER_LABEL[r.trigger] ?? r.trigger}</td>
|
||||
<td>
|
||||
<span className={runPillClass(r.status)}>{RUN_STATUS_LABEL[r.status] ?? r.status}</span>
|
||||
</td>
|
||||
<td className="num">{count(r, "new_drafts")}</td>
|
||||
<td className="num">{count(r, "drafts_updated")}</td>
|
||||
<td className="num">{count(r, "cos_opened") + count(r, "cos_reaimed")}</td>
|
||||
<td className="num">{attached?.get(r.id) ?? count(r, "attached")}</td>
|
||||
<td className="small">
|
||||
{r.errors.length ? (
|
||||
<span className="due over" title={r.errors.join("\n")}>
|
||||
{r.errors.length === 1 ? r.errors[0] : `${r.errors.length} errors: ${r.errors[0] ?? ""}`}
|
||||
</span>
|
||||
) : (
|
||||
<span className="muted">None</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user