Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
ci / server (pull_request) Successful in 12m48s
ci / frontend (pull_request) Successful in 54s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Successful in 4m38s

- C1 (issue 1): over target follows VISUAL ticket hours, not the timer.
- C2 (issue 24): a Q.C. reject always needs a reason; accept needs an NCR number after a reject without one.
- C3 (issue 25): an incomplete unit ITP blocks filing.
- C4 (issue 27): prints open the print dialog on the page, no new tab.
- C5 (issue 31): filing is blocked while VISUAL shows a work order open; the feed sets vclosed on VISUAL packets.
- C6 (issue 8): the data book leaves out the history pages.
- C7 (issue 18): the supervisor approves each received short; accept and filing wait for it.
- C8 (issue 23): per-department checklists, edited by the department supervisor or Admin, frozen at START.
- C9 (issue 32): Q.C. and Clerical may run the stamp check.
- C10 (issue 33): the PM screen and reports are for PS-ProjectMgmt and Admin only.
- C11 (issue 36): the Stall screen lists the packets assigned to the welder, at the supervisor's stall.

Migration 0011. Docs: PLAN (also issue 9 confirmed, issue 4 asked), POLICY, SECURITY, SCHEMA, FEED, README, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-30 16:36:49 -06:00
co-authored by Claude Opus 5.5
parent 150c507a31
commit 70a97398f1
101 changed files with 3871 additions and 1098 deletions
+22 -26
View File
@@ -2,7 +2,7 @@
Inside one transaction: lock the packet row (SELECT ... FOR UPDATE), build the policy's PacketState, check the
caller's expected step (409 stale_step), check the packet version when If-Match is given (409 version_conflict),
ask the policy (403 or 400 with the decision's code), take the signature when the rule needs one, hand the caller a
ask the policy (403, 400 or 422 with the decision's code), take the signature when the rule needs one, hand the caller a
`Write`, then bump the version, write the packet_log row, the transition row and its side effects, the audit row and
the NOTIFY. The policy decides; a handler never does.
@@ -38,7 +38,7 @@ from ..auth.signatures import Signature
from ..policy.engine import MSG_FILED
from ..settings import SecretFileError
from ..text import LIMITS, clean_opt, reason_ok
from . import packets
from . import checklists, packets
from . import qc as qc_dom
log = logging.getLogger(__name__)
@@ -52,14 +52,6 @@ MSG_FIELD = "Someone else changed this since you opened it."
MSG_DATABOOK = "The data book could not be built: {reason}. Nothing was filed."
def open_wos_text(wos: Sequence[str]) -> str:
"""The FILE confirm question: "VISUAL still shows 2 work orders open on this packet: L489052/1, L489052/2. File
anyway?" """
n = len(wos)
what = "1 work order" if n == 1 else f"{n} work orders"
return f"VISUAL still shows {what} open on this packet: {', '.join(wos)}. File anyway?"
# Codes that answer 400 (the UI asks for input and retries); every other refusal answers 403.
ASK_CODES = frozenset({"it_reason_required", "reason_required"})
@@ -202,7 +194,7 @@ def refuse(decision: policy.Decision) -> None:
"""Raise the API error for a failed policy decision."""
if decision.ok:
return
status = 400 if decision.code in ASK_CODES else 403
status = 400 if decision.code in ASK_CODES else 422 if decision.code == "ncr_required" else 403
raise ApiError(status, decision.code, decision.why)
@@ -348,11 +340,6 @@ def packet_write(
bypass = bypass or dd.admin_bypass
if if_match is not None and int(if_match) != int(row["version"]):
raise ApiError(409, "version_conflict", MSG_VERSION, extra={"current": current_of(row)})
if action is A.FILE and extra.get("confirm_open_wos") is not True:
# Before the signature: a cancelled confirm costs no attempt and writes nothing.
wos = packets.packet_open_wos(conn, row)
if wos:
raise ApiError(409, "open_work_orders", open_wos_text(wos), extra={"work_orders": wos})
w = Write(
conn=conn,
principal=p,
@@ -555,6 +542,13 @@ def _set_result(w: Write, insp: Mapping[str, Any] | None, result: str, note: str
)
def _set_ncr(w: Write, insp: Mapping[str, Any] | None) -> None:
"""The NCR number the step carries (a reject, or the accept after a reject without one), on the inspection."""
ncr = w.extra.get("ncr") or None
if insp is not None and (ncr or w.action is A.QC_REJECT):
w.conn.execute("UPDATE qc_inspections SET ncr = %s WHERE id = %s", (ncr, insp["id"]))
def _set_packet(w: Write, assignments: str, params: Sequence[Any] = ()) -> None:
w.conn.execute(f"UPDATE packets SET {assignments} WHERE id = %s", (*params, w.id)) # noqa: S608 (fixed fragments)
@@ -685,6 +679,8 @@ def apply_transition(w: Write) -> None:
assert to_status is not None
ex = w.extra
reason = _reason(w)
if a is A.QC_REJECT and reason and ex.get("ncr"):
reason = f"{ex['ncr']} · {reason}" # the supervisor's return note leads with the NCR number
from_dept = w.from_dept
to_dept = from_dept
if rule.dept_change != "no" and isinstance(ex.get("dept"), str) and ex.get("dept"):
@@ -751,6 +747,8 @@ def apply_transition(w: Write) -> None:
"clock_running_since = coalesce(clock_running_since, now()),"
" clock_started_at = coalesce(clock_started_at, now())",
)
# change C8: the stage keeps the checklist it started with
checklists.freeze_on_start(w.conn, w.id)
elif a is A.SUBMIT:
_set_packet(w, "return_note = NULL")
_stage_update(w, CLOCK_STOP)
@@ -776,6 +774,7 @@ def apply_transition(w: Write) -> None:
elif a in (A.ROUTE_FROM_REVIEW, A.QC_ACCEPT_AND_ROUTE):
insp = _open_inspection(w)
if a is A.QC_ACCEPT_AND_ROUTE:
_set_ncr(w, insp)
insp = _set_result(w, insp, "accepted", _note(w))
_close_stage(w, qc_snapshot=insp)
_close_inspection(w, insp, from_dept)
@@ -784,15 +783,16 @@ def apply_transition(w: Write) -> None:
elif a is A.QC_REJECT:
# The reject closes the inspection into qc_history at once; the next SEND_TO_QC opens a fresh one.
insp = _open_inspection(w)
if insp is not None:
w.conn.execute("UPDATE qc_inspections SET ncr = %s WHERE id = %s", (ex.get("ncr") or None, insp["id"]))
_set_ncr(w, insp)
insp = _set_result(w, insp, "rejected", reason)
_close_inspection(w, insp, from_dept)
_set_packet(w, "return_note = %s", (reason,))
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) # a draft sent straight to Q.C. has no stage yet
elif a is A.QC_ACCEPT:
insp = _set_result(w, _open_inspection(w), "accepted", _note(w))
insp = _open_inspection(w)
_set_ncr(w, insp)
insp = _set_result(w, insp, "accepted", _note(w))
_close_inspection(w, insp, from_dept)
_close_stage(w, qc_snapshot=insp)
_set_packet(w, "routed_to = %s::route_kind", (str(ex["routed_to"]),))
@@ -838,20 +838,16 @@ def _file(w: Write) -> None:
_set_packet(w, "databook_at = now()")
row = packets.load_row(w.conn, w.id)
assert row is not None
wos = packets.packet_open_wos(w.conn, row) if w.extra.get("confirm_open_wos") is True else []
content = packets.seal_content(w.conn, row)
digest = seal_hash(key, content)
kid = seal_key_id(key)
seal_id, sealed_at = w.conn.execute(
"INSERT INTO packet_seals (packet_id, hash, key_id, sealed_by, signature_id, open_wos)"
" VALUES (%s, %s, %s, %s, %s, %s) RETURNING id, sealed_at",
(w.id, digest, kid, w.user_id, w.signature.id if w.signature else None, wos or None),
"INSERT INTO packet_seals (packet_id, hash, key_id, sealed_by, signature_id)"
" VALUES (%s, %s, %s, %s, %s) RETURNING id, sealed_at",
(w.id, digest, kid, w.user_id, w.signature.id if w.signature else None),
).fetchone()
pages = _file_databook(w, row, {"hash": digest.hex(), "key_id": kid, "sealed_at": sealed_at}, int(seal_id))
w.log_also("Data book filed", f"{pages} pages")
if wos:
w.note = " · ".join(x for x in (_note(w), "Filed with work orders open in VISUAL: " + ", ".join(wos)) if x)
w.detail["open_wos"] = wos
w.detail["seal_id"] = int(seal_id)
w.detail["databook_pages"] = pages