Phase 5: Supervisor and Stall screens, weld map, heat table, clock and CO verification
ci / frontend (push) Successful in 34s
ci / server (push) Successful in 7m15s
ci / image (push) Successful in 39s
ci / e2e (push) Successful in 2m4s

The shop at parity with the old build. Supervisor screen (department chips, CO headline, numbers, Change orders
on hold, station grid with clocks and CO tags, boards) and Stall screen (station picker, hero, My packets,
Submitted). Shop steps on the packet page: assign with station, welder picker and stamp; pull back; Start, Pause,
Resume; Submit with a PIN signature and the missing steps as links; review with Send to Q.C., Return to station
and Skip Q.C. route; a lead's read-only review. Weld map (pdf.js viewer with pan, pinch, zoom, full screen; marks
with leader lines; weld sheet; weld record with field CAS; carried marks). Heat map (heat table placed, dragged,
sized, removed; legacy amber tags; borrowed heat map read-only). Checklist tab with templates from settings.
VOID stamps on voided pages. Change orders: CO-n labels, supervisor verify with signature, approval copies the
heat table to the new sheet and the BOM merge keeps the shop's values. Server clock of record with VISUAL ticket
hours beside it. Presence per packet over SSE. Migration 0006 (bfx, bfy, carried_from, checklist_templates).
New endpoints /api/shop/board, /api/shop/stations, /api/shop/stall, /api/departments/{code}/people.

Tests: policy oracle for every shop action, role and status; clock arithmetic; concurrent inserts; CO apply;
frontend geometry, heat table, clock, checklist, weld sheet, screens; seven Playwright scenarios. Docs updated.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-10-16 22:46:56 -06:00
co-authored by Claude Fable 5.1
parent 39a4eb5213
commit 5a4174e0c2
81 changed files with 11532 additions and 352 deletions
+92 -2
View File
@@ -63,7 +63,7 @@ LABELS: dict[policy.Action, str] = {
A.UNASSIGN: "Taken off the station",
A.PULL_BACK: "Pulled back from the station",
A.START: "Work started",
A.SUBMIT: "Submitted to the supervisor",
A.SUBMIT: "Submitted to supervisor",
A.RETURN_TO_STATION: "Returned to the station",
A.SEND_TO_QC: "Sent to Q.C.",
A.ROUTE_FROM_REVIEW: "Routed to another department",
@@ -127,6 +127,8 @@ class Write:
noop: bool = False
from_status: str = ""
from_dept: str = ""
children: Any = None # packets.Children loaded under the lock (full=False)
more_logs: list[tuple[str, str | None]] = field(default_factory=list)
@property
def id(self) -> int:
@@ -146,6 +148,10 @@ class Write:
if note is not None:
self.note = note
def log_also(self, action_label: str, note: str | None = None) -> None:
"""One more packet_log line for this write, written after the main one (same user, no signature)."""
self.more_logs.append((action_label, note))
def changed(self, *names: str) -> None:
"""Record the fields this write changed (they go into the audit detail)."""
for n in names:
@@ -331,6 +337,7 @@ def packet_write(
request=request,
from_status=str(row["status"]),
from_dept=str(row["dept_code"]),
children=ch,
)
meaning = decision.signature
needs = bool(meaning) if require_sign is None else require_sign
@@ -398,6 +405,20 @@ def _finish(w: Write) -> None:
w.transition_id,
),
)
for extra_label, extra_note in w.more_logs:
conn.execute(
"INSERT INTO packet_log (packet_id, by, by_name, role, action, note, it_reason)"
" VALUES (%s, %s, %s, %s, %s, %s, %s)",
(
w.id,
w.user_id,
w.principal.user["display_name"],
w.principal.role_snapshot,
extra_label,
clean_opt(extra_note, LIMITS["note"]) if extra_note else None,
w.it_reason,
),
)
detail: dict[str, Any] = {"from": w.from_status, "to": status, "dept": dept}
if w.from_dept != dept:
detail["from_dept"] = w.from_dept
@@ -563,6 +584,64 @@ def _release_text(w: Write) -> None:
w.log(LABELS[A.WH_RELEASE], note)
def hours_text(ms: Any) -> str:
"""Stage clock hours with one decimal: 1.5 h."""
return f"{int(ms or 0) / 3600000:.1f} h"
def _station_name(w: Write, station_id: Any) -> str | None:
if station_id is None:
return None
ch = w.children
if ch is not None and ch.station and ch.station.get("id") == station_id:
return str(ch.station["name"])
r = w.conn.execute("SELECT name FROM stations WHERE id = %s", (station_id,)).fetchone()
return str(r[0]) if r else None
def _dept_text(w: Write, code: str) -> str:
labels = getattr(w.children, "dept_labels", None) or packets.dept_labels(w.conn)
return str(labels.get(code, code))
def _shop_texts(w: Write, a: policy.Action, to_dept: str, stamp: str | None = None) -> None:
"""The packet_log label and note of a shop step (the old click handlers' texts)."""
ex = w.extra
here = _station_name(w, w.packet["station_id"]) or "the station"
note = _note(w)
if a is A.ASSIGN:
station = _station_name(w, int(ex["station_id"])) or "a station"
r = w.conn.execute("SELECT display_name FROM users WHERE id = %s", (str(ex["welder_id"]),)).fetchone()
who = str(r[0]) if r else ""
w.log(f"Sent to {station}", f"{who} [{stamp}]" if stamp else who)
elif a is A.UNASSIGN:
w.log(f"Taken off {here}", note)
elif a is A.PULL_BACK:
w.log(f"Pulled back from {here}", note)
elif a is A.START:
w.log(LABELS[A.START], " · ".join(x for x in (here if w.packet["station_id"] else None, note) if x) or None)
elif a is A.SUBMIT:
r = w.conn.execute(
"SELECT clock_elapsed_ms FROM packet_stages WHERE packet_id = %s AND closed_at IS NULL", (w.id,)
).fetchone()
n = packets.dept_welds(w.packet, w.children) if w.children is not None else 0
text = f"{n} welds · {hours_text(r[0] if r else 0)}"
w.log(LABELS[A.SUBMIT], f"{text} · {note}" if note else text)
elif a is A.RETURN_TO_STATION:
w.log(f"Returned to {here}", _reason(w))
elif a is A.SEND_TO_QC:
loc = clean_opt(ex.get("location"), LIMITS["location"])
w.log(LABELS[A.SEND_TO_QC], " · ".join(x for x in (f"Spool at {loc}" if loc else None, note) if x) or None)
elif a is A.ROUTE_FROM_REVIEW:
text = f"Supervisor, Q.C. skipped · from {_dept_text(w, w.from_dept)}"
w.log(f"Routed to {_dept_text(w, to_dept)}", f"{text} · {note}" if note else text)
SHOP_TEXTS = frozenset(
{A.ASSIGN, A.UNASSIGN, A.PULL_BACK, A.START, A.SUBMIT, A.RETURN_TO_STATION, A.SEND_TO_QC, A.ROUTE_FROM_REVIEW}
)
def apply_transition(w: Write) -> None:
"""Set status, status_at and dept, write the packet_transitions row and run the action's side effects."""
if w.transitioned:
@@ -579,6 +658,10 @@ def apply_transition(w: Write) -> None:
if rule.dept_change != "no" and isinstance(ex.get("dept"), str) and ex.get("dept"):
to_dept = str(ex["dept"])
stamp: str | None = None
if a in SHOP_TEXTS and a is not A.ASSIGN and a is not A.SUBMIT:
_shop_texts(w, a, to_dept) # before the side effects clear the station
if a in (A.SEND_TO_WAREHOUSE, A.VIS_AUTO_RELEASE):
_set_packet(w, "wh_skipped = false, wh_sent_at = now(), return_note = NULL")
_wh_incoming(w)
@@ -623,14 +706,21 @@ def apply_transition(w: Write) -> None:
if not packets.one(w.conn, "SELECT id FROM packet_stages WHERE packet_id = %s AND closed_at IS NULL", (w.id,)):
_open_stage(w, from_dept)
_stage_update(w, "station_id = %s, welder_id = %s, welder_stamp = %s", (station_id, welder_id, stamp))
_shop_texts(w, a, to_dept, stamp)
elif a in (A.UNASSIGN, A.PULL_BACK):
_set_packet(w, "station_id = NULL, welder_id = NULL, welder_stamp = NULL")
_stage_update(w, f"{CLOCK_STOP}, station_id = NULL, welder_id = NULL, welder_stamp = NULL")
elif a is A.START:
_stage_update(w, "clock_running_since = now(), clock_started_at = coalesce(clock_started_at, now())")
# A clock that is already running keeps its start; only the server sets it.
_stage_update(
w,
"clock_running_since = coalesce(clock_running_since, now()),"
" clock_started_at = coalesce(clock_started_at, now())",
)
elif a is A.SUBMIT:
_set_packet(w, "return_note = NULL")
_stage_update(w, CLOCK_STOP)
_shop_texts(w, a, to_dept)
elif a is A.RETURN_TO_STATION:
_set_packet(w, "return_note = %s", (reason,))
elif a is A.SEND_TO_QC: