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
+9 -3
View File
@@ -272,7 +272,13 @@ The apply runs in one transaction with a 5-second lock timeout. A packet row loc
5. **Past draft: change orders** (action `vis_open_co`). See "Change orders from the feed". Filed packets get nothing.
6. **Retire moved drafts** (action `vis_retire_draft`). A live draft made by the feed, with no drawing pages and no hand uploads, that is not among this run's candidates, and whose every leg now sits on another live candidate, is soft-removed with reason `moved`. `visual.moved` records where the legs went. Log "Retired: work orders moved to another packet". It is hidden from the boards and, being removed, is never recreated.
7. **Work order gone.** Skipped on a suspect batch. Each tracked leg VISUAL did not return gets `missing_runs + 1`. At 2 the leg gets `gone_at`, the packet gets flag `wo_gone` and `wo_gone_at`, and the log says "Work order <key> no longer in VISUAL". A leg that comes back resets to 0. When no leg is gone any more the flag goes and the log says "Work order back in VISUAL". The feed never removes a packet for this.
8. **Closed and cancelled.** Each leg's status code is stored, with ticket hours and the time observed. When every leg is C, `visual_closed_at` is set and the log says "Closed in VISUAL (observed)". Any leg X sets flag `cancelled` and `visual_cancelled_at`. The flag goes when no leg is X. A cancelled draft is not removed.
8. **Closed and cancelled.** Each leg's status code is stored, with ticket hours and the time observed. When every leg is C, `visual_closed_at` is set. Any leg X sets flag `cancelled` and `visual_cancelled_at`. The flag goes when no leg is X. A cancelled draft is not removed.
Since change C5 (Gitea issue 31, built 2026-10-30) the feed also sets and clears the packet's close in VISUAL, `vclosed_at`, on a VISUAL packet that is not filed or removed. It is the one writer there; Clerical sets it only on hand packets.
- When every leg has a status and each is C or X, the feed sets `vclosed_at` to now, with `vclosed_by` null (an existing value is kept). The log says "Closed in VISUAL (observed)" and the run counts `closed_observed`. The Clerical board then moves the packet from "Ready to close" to "Ready for data book" by itself.
- When a leg shows open again (a status that is not C or X) on a packet with `vclosed_at` set, the feed clears `vclosed_at`, `vclosed_by` and `visual_closed_at`. The log says "Open again in VISUAL (observed)" with the open work orders as the note. No count.
- The feed does this in any status before filed, so a packet can come back from Q.C. already "Ready for data book". A filed packet is never touched.
- FILE on a VISUAL packet needs `vclosed_at` set and no leg open, on both routes (precondition `closed_in_visual`, see [POLICY.md](POLICY.md), "Filing").
- A leg flagged `wo_gone` keeps its last status. If that status was open, the packet cannot file. Asked on Gitea issue 31 on 2026-10-30: count a leg gone for 2 runs as closed, or let an Admin close it with a reason.
9. **Auto-release** when `auto_release` is on and the feed is not paused: up to `auto_release_max` (default 25) live drafts made by the feed with the drawing attached, no flags at all, and the rev confirmed by the drawing job. A draft whose heat map is unanswered gets "yes" when it has a real material line (not a sub-assembly, not n/a, not consumable) and "no" otherwise. Then action `vis_auto_release` moves it to the warehouse with an automatic warehouse stage row and a transition row. The policy's preconditions (heat map answered, drawing attached) still apply.
10. **Summary.** One audit row `feed.run` with the counts, and one `feed` event to the screens after commit.
@@ -417,7 +423,7 @@ Flags live in `packets.flags`. A draft with any flag shows in Clerical's Flagged
With both hand flags the text is "Drawing and BOM uploaded by hand - check it and send it on". Hand drafts keep their phase 2 text.
"Closed in VISUAL" is not a flag. It is an observed state (`visual_closed_at`), shown on the packet. Filing a Data Book packet needs it, as the `vclosed_if_databook` precondition says.
"Closed in VISUAL" is not a flag. It is an observed state, shown on the packet: `vclosed_at` when every leg is closed or cancelled, `visual_closed_at` when every leg is C. Filing a VISUAL packet needs `vclosed_at` and no open leg, on both routes, as the `closed_in_visual` precondition says (change C5).
### Hand uploads
@@ -506,7 +512,7 @@ The packet read model has a `labor` field from `visual_labor`. It joins the pack
`labor` is null for a hand packet, or when no `visual_labor` row matches. The packet page shows the sum beside the station clock, as `VISUAL tickets 3.2 h`.
The station timer stays the clock of record (open question 1, default kept). The ticket hours are shown for comparison only. Nothing is computed from them, and they never change the clock. The screen reads them from PostgreSQL, never from VISUAL.
The station timer stays and is shown as before. Since change C1 (Gitea issue 1, built 2026-10-30) the ticket hours decide "over target" on the Project Management reports: ticket hours above the work-order target. A packet with no ticket hours is not over target. The ticket hours never change the clock. The screen reads them from PostgreSQL, never from VISUAL.
## What the screens read
+38 -30
View File
@@ -145,7 +145,8 @@ Each phase ends with passing tests, docs updated (README, CHANGELOG, SECURITY.md
| D1, D2, D4 | Deferred (envelope encryption, Key Vault) |
| D3 | IT's (host disk encryption on docker-1) |
| D5 | Closed, phase 4 (optional ClamAV) |
| D6, D7 | Phase 10 (decided 2026-10-28, Gitea issues 7 and 8: nothing is purged and filed packets are archived to the QNAP share at filing; the one data book leaves out the history pages) |
| D6 | Phase 10 (decided 2026-10-28, Gitea issue 7: nothing is purged and filed packets are archived to the QNAP share at filing) |
| D7 | Closed 2026-10-30 (Gitea issue 8, change C6: the one data book goes to the customer and leaves out the history pages) |
| M1 | Closed, phase 2 |
| M2 | Closed, phase 7 |
| M3 | Closed, phase 9 |
@@ -181,7 +182,7 @@ Conventions: identity bigints except users (uuid = AD objectGUID); Postgres enum
- **Inputs:** `Principal` (user, admin, support set, clerical_supervisor, shop dept to level map, station limits, session kind, or `system="visual-feed"`) and `PacketState` (status, dept, station, wh_skipped, routed_to, vclosed, removed, open COs, open shorts). Pure functions in `server/packetstation/policy/`, no I/O.
- **Custody** `holds()` is a direct port: draft CL; warehouse WH; ready and review dept lead or above; assigned and in_progress dept lead or above or works_on (worker in dept whose station limits include the station, and, tightened per the README, the assigned welder for start and submit); qc QC; complete CL; filed nobody.
- **Action enum** with a rule per action: who, from and to status, dept change, CO hold, preconditions, signature meaning, reason required. Transitions: SEND_TO_WAREHOUSE, RELEASE_TO_SHOP, SEND_DRAFT_TO_QC, RETURN_TO_CLERICAL, WH_RELEASE, WH_SEND_BACK, SEND_BACK_TO_WAREHOUSE, ASSIGN, UNASSIGN, PULL_BACK, START, SUBMIT, RETURN_TO_STATION, SEND_TO_QC, ROUTE_FROM_REVIEW, QC_REJECT, QC_ACCEPT, QC_ACCEPT_AND_ROUTE, FILE, ADMIN_REOPEN. Edits: EDIT_BOM, WH_RECEIVE_SHORT (a narrow endpoint that replaces onlyShortLines), EDIT_HEATS, EDIT_WELDS, EDIT_CHECKLIST, EDIT_STAGE_FIELDS, CLOCK, EDIT_QC, SET_WH_STAGE, EDIT_DEPT, MOVE_UNIT, EDIT_WOS, EDIT_DOCUMENT_SET, EDIT_HEADER, REV_APPROVE (clerical supervisor), SET_VCLOSED, SET_DATABOOK_AT, DOC_ADD, DOC_REMOVE, CO_ISSUE, CO_APPROVE, CO_REJECT, CO_HOLD_TOGGLE, CO_VERIFY, ITP_*, VIS_* (worker only).
- **Preconditions** as named predicates, now server-side: heat_map_answered, drawing_attached, bom_all_pulled_or_short, welder_set, checklist_complete, welds_recorded, filler_set, qc_book_spool_location, qc_missing_empty, reason_given, vclosed_if_databook (warning only when work orders are still open in VISUAL; block only when vclosed is unset and the route is Data Book), co_staged_if_dwg_change.
- **Preconditions** as named predicates, now server-side: heat_map_answered, drawing_attached, bom_all_pulled_or_short, welder_set, checklist_complete, welds_recorded, filler_set, qc_book_spool_location, qc_missing_empty, reason_given, closed_in_visual (replaced vclosed_if_databook on 2026-10-30, change C5: a VISUAL packet files only when the feed has seen every work order closed, on both routes; a hand packet on the Data Book route needs the Clerical confirm), itp_complete (C3), shorts_approved (C7), ncr_given (C2), co_staged_if_dwg_change.
- **Order of checks** keeps the old semantics: removed, wrong status, CO hold (applies to admins too), admin bypass of custody with a required IT reason, filed lock (only SET_DATABOOK_AT and ADMIN_REOPEN), who and custody, then preconditions (never bypassed).
- **`can` on reads:** every packet read returns `can: [...]` (passes role, custody, status and preconditions), `blocked: {action: [reasons]}` (passes role and custody but fails a precondition or the CO hold, so the UI can show a disabled control with the reason), `holder`. Boards evaluate transitions only from header columns plus an open-CO count.
- **Signatures:** required for SUBMIT, SEND_TO_QC, QC_ACCEPT, QC_ACCEPT_AND_ROUTE, QC_REJECT, FILE, ROUTE_FROM_REVIEW, ADMIN_REOPEN, CO_ISSUE, CO_VERIFY, CO_APPROVE, CO_REJECT, REV_APPROVE, hand uploads, ITP_BLOCK_SIGN, ITP_SIGN_OFF. The body carries `sign: {factor, secret}`; the server verifies the factor against the session's own user inside the transaction (PIN allowed in any session on any device, as the brief says; password everywhere), logs the attempt, applies the five-strike PIN disable and the three-strike session end, and writes the signatures row with a content hash over what was signed.
@@ -217,19 +218,19 @@ Conventions: identity bigints except users (uuid = AD objectGUID); Postgres enum
Need an answer before the named phase. Defaults apply if none arrives in time.
1. **Station timer** (Seth, before phase 5): keep, cross-check against labor tickets, or drop. Default: keep the timer, show ticket hours beside it. Phase 5 (2026-10-16) applied the default: the station timer is the clock of record and the VISUAL ticket hours are shown beside it. Decided by SethD on 2026-10-26, Gitea issue 1: keep the timer as built, but flag over target on the VISUAL ticket hours (change C1 below).
1. **Station timer** (Seth, before phase 5): keep, cross-check against labor tickets, or drop. Default: keep the timer, show ticket hours beside it. Phase 5 (2026-10-16) applied the default: the station timer is the clock of record and the VISUAL ticket hours are shown beside it. Decided by SethD on 2026-10-26, Gitea issue 1: keep the timer as built, but flag over target on the VISUAL ticket hours (change C1 below). Built on 2026-10-30.
2. **PDFs on the server** (confirm before phase 7): reportlab + pypdf + pypdfium2. Default: yes. Phase 7 (2026-10-17) applied the default: reportlab 5.0.1, pypdf and pypdfium2, with Pillow 13.0.0 for the rasters.
3. **Frontend stack** (confirm before phase 1): TypeScript + React + Vite. Default: yes. Plain JavaScript would not be faster for parity: the old 7,000 lines are screen code that must be rewritten either way, and typed API models cut rework.
4. **PIN defaults** (confirm): 6 to 12 digits, five attempts, 10-hour shop session, device cookie. Was open (Seth and Q.C., before phase 6): whether Q.C. accept and filing require the domain password even with a PIN set. Default: PIN is enough. Confirmed by SethD on 2026-10-26, Gitea issue 2: the PIN is enough for every sign-off, Q.C. accept and filing included. IT: device certificate later. The device cookie stays and no device certificate is planned: confirmed by Spencer (IT) on 2026-10-18, Gitea issue 3.
5. **Several open orders on a project** (before phase 3): which order a packet shows. Proposed rule: the open order whose line's top job is the packet's work order base, else the newest open order, else the newest closed one; the packet shows "via base" or "via project" and Clerical can override. Needs Seth's view. Phase 3 (2026-10-16) built the default in one function (`feed/orders.py`): a Clerical pin, else the base's own linked order, else the project order whose top job is the unit, else the project's first open order, else its newest; the source (`via`) is stored on the project and unit and shown on the project page, which lists every open order. Seth may change the rule later in that one place. SethD on 2026-10-28, Gitea issue 4: the order should follow the shop schedule, linked live so a job that is moved, pushed or pulled up changes what its packets show. That needs a discussion first; the rule stays as built until then.
5. **Several open orders on a project** (before phase 3): which order a packet shows. Proposed rule: the open order whose line's top job is the packet's work order base, else the newest open order, else the newest closed one; the packet shows "via base" or "via project" and Clerical can override. Needs Seth's view. Phase 3 (2026-10-16) built the default in one function (`feed/orders.py`): a Clerical pin, else the base's own linked order, else the project order whose top job is the unit, else the project's first open order, else its newest; the source (`via`) is stored on the project and unit and shown on the project page, which lists every open order. Seth may change the rule later in that one place. SethD on 2026-10-28, Gitea issue 4: the order should follow the shop schedule, linked live so a job that is moved, pushed or pulled up changes what its packets show. That needs a discussion first; the rule stays as built until then. SethD on 2026-10-29: the schedule is Presley's Production Planner, which the whole shop works from today. Asked on issue 4 (2026-10-30): what it is, where it lives, what a row holds and how often it changes. Packet Station would only read it.
6. **VISUAL_ENCRYPT** (IT, before production): yes once vsql has a trusted certificate; `no` until then.
7. **Policy tightenings** listed under "Challenges" 1 to 3 (Seth, before phase 2). Confirmed by SethD on 2026-10-26, Gitea issue 6: "good as shown". Every row is kept, filing for packets routed to Shipping included (Spencer read the answer that way on 2026-10-27).
8. **Repo location**: PacketStation2 as proposed, or another name.
9. **Retention period** (D6) and customer-facing data book copy (D7): deferred, but a decision from Seth and Q.C. lets a later phase close them. Gitea issue 7: SethD expects all data to be kept (2026-10-26). Spencer decided on 2026-10-27 that project files are also archived to an NFS share he sets up on the QNAP, with an Admin setting for the folder path that checks read and write (phase 10, "Archive location"). Nothing is deleted from Packet Station; the archive is a copy. Still open on issue 7: a minimum retention period, if any, and what goes into the archive and when. Decided on 2026-10-28: all data is kept and nothing is purged; at filing the data book and each of the packet's documents go to the archive with a manifest (SethD, Gitea issue 7). There is one data book, the customer copy, without the history pages (SethD, Gitea issue 8, change C6).
9. **Retention period** (D6) and customer-facing data book copy (D7): deferred, but a decision from Seth and Q.C. lets a later phase close them. Gitea issue 7: SethD expects all data to be kept (2026-10-26). Spencer decided on 2026-10-27 that project files are also archived to an NFS share he sets up on the QNAP, with an Admin setting for the folder path that checks read and write (phase 10, "Archive location"). Nothing is deleted from Packet Station; the archive is a copy. Still open on issue 7: a minimum retention period, if any, and what goes into the archive and when. Decided on 2026-10-28: all data is kept and nothing is purged; at filing the data book and each of the packet's documents go to the archive with a manifest (SethD, Gitea issue 7). There is one data book, the customer copy, without the history pages (SethD, Gitea issue 8, change C6). C6 was built on 2026-10-30, which closes D7. D6 (the archive) is phase 10.
Smaller questions from the design, with the default the build takes unless told otherwise:
10. Same-rev drawing re-release (new file, same rev) on an in-shop packet: default a drawing-origin change order, not a silent swap.
10. Same-rev drawing re-release (new file, same rev) on an in-shop packet: default a drawing-origin change order, not a silent swap. Confirmed by SethD on 2026-10-29, Gitea issue 9: keep the change order as built, so the shop confirms the new pages. A byte-identical file is still ignored.
11. Cancelled (X) work orders: default flag only, like gone; drafts are not removed. SethD on 2026-10-26, Gitea issue 10: the default is fine, left to Spencer. Kept as built.
12. Partspecs, title or folded-operation target changes on an in-shop packet: default no change order, as the old build. Confirmed by SethD on 2026-10-28, Gitea issue 11: only a new drawing rev, a quantity change or a removed operation opens a change order.
13. Work-order-gone confirmation after 2 consecutive runs and the 10% suspect-batch guard: defaults as stated. SethD left both numbers to Spencer on 2026-10-26, Gitea issue 12. The defaults stay unless Spencer changes them.
@@ -243,7 +244,7 @@ From phase 4 (2026-10-16), for Seth to veto:
19. The release gate has two server rules: every line not N/A is pulled or on an open short, and every pulled line that issued pieces has a heat unless it is N/A or a sub-assembly. The old build enforced both on the screen only. Default: both on the server. Confirmed by SethD on 2026-10-26, Gitea issue 16: both stay, so the warehouse fills the heat map for the parts it sends.
20. Hold and short reasons: the old build accepted any text. Default: closed sets on the server; "Other" on a hold needs a note. Confirmed by SethD on 2026-10-28, Gitea issue 17: keep the lists.
21. Receiving a short on a packet the warehouse no longer holds: default it stays a warehouse right (WH_RECEIVE_SHORT, any status but filed), and the holder of the packet (department lead or supervisor, Q.C., Clerical) can receive too, through edit_bom. Changed by SethD on 2026-10-28, Gitea issue 18 (change C7).
21. Receiving a short on a packet the warehouse no longer holds: default it stays a warehouse right (WH_RECEIVE_SHORT, any status but filed), and the holder of the packet (department lead or supervisor, Q.C., Clerical) can receive too, through edit_bom. Changed by SethD on 2026-10-28, Gitea issue 18 (change C7). Built on 2026-10-30: receiving works as before, and the department supervisor then approves each receipt.
22. Upload kinds and limits: default an MTR is PDF, PNG or JPG up to 25 MB, a hand BOM is XLSX or CSV up to 5 MB. ClamAV is optional and off unless CLAMAV_HOST is set; when set, an unreachable scanner refuses uploads (fail closed). Confirmed by Spencer (IT) on 2026-10-18, Gitea issue 19: the limits stay, and ClamAV stays off in both stacks because every workstation that uses Packet Station runs Microsoft Defender. The `clamav` profile stays in the build, unused.
23. The quota alert: default a `notification_events` row (phase 9 builds the delivery on this table) at 85 percent of the assets volume. Confirmed by Spencer (IT) on 2026-10-18, Gitea issue 20: 85 percent stays, and the alert goes to PS-Admins. Phase 9 seeds the `assets_quota` notification rule with PS-Admins as its recipient.
@@ -251,32 +252,32 @@ From phase 5 (2026-10-16), for Seth to veto:
24. Carried marks: when a packet is routed to another department, the welds, heat tags and heat table stay on the packet. The read model computes a `carried` flag. Nothing is copied. Default: computed, not copied. Confirmed by SethD on 2026-10-26, Gitea issue 21: the marks stay with the packet as it moves on, and a department that receives the packet (for example weld assembly after parts weld) adds its welds to the same weld map, so the data book carries one map.
25. The old build's manual "copy marks to the new sheet" button on a change order is not ported. On a page replacement only the heat table placement is copied to the new page. Welds and heat tags stay on the VOID page. Default: not ported in phase 5; a follow-up. Confirmed by SethD on 2026-10-26, Gitea issue 22: not ported. After a change order the department re-verifies the work, since a change can alter what was marked.
26. Checklist templates live in the `checklist_templates` settings row, with the old lists as defaults. Default: no screen edits them in phase 5; Admin editing comes in phase 9. Changed by SethD on 2026-10-26 and 2026-10-28, Gitea issue 23 (change C8).
26. Checklist templates live in the `checklist_templates` settings row, with the old lists as defaults. Default: no screen edits them in phase 5; Admin editing comes in phase 9. Changed by SethD on 2026-10-26 and 2026-10-28, Gitea issue 23 (change C8). Built on 2026-10-30: the `checklist_templates` row now holds the default lists by kind, used by a department that has no list of its own.
From phase 6 (2026-10-17), for Seth and Q.C. to veto:
27. Q.C. reject: the old build took any non-empty reason. Default: a reason of at least 8 characters, or a well-formed NCR number (`NCR-YYYY-NNNN`) in its place, or both. The NCR is stored on the inspection and linked to pesco-ncr. Applied in phase 6. Changed by SethD on 2026-10-26, Gitea issue 24 (change C2 below).
28. A filed packet in a unit and the unit's ITP: the ITP belongs to the unit, not to a packet, so ITP writes never look at a packet's status. Default: a filed packet does not lock the ITP. This resolves the phase 5 follow-up. Applied in phase 6. Changed by SethD on 2026-10-26, Gitea issue 25 (change C3 below).
27. Q.C. reject: the old build took any non-empty reason. Default: a reason of at least 8 characters, or a well-formed NCR number (`NCR-YYYY-NNNN`) in its place, or both. The NCR is stored on the inspection and linked to pesco-ncr. Applied in phase 6. Changed by SethD on 2026-10-26, Gitea issue 24 (change C2 below). Built on 2026-10-30.
28. A filed packet in a unit and the unit's ITP: the ITP belongs to the unit, not to a packet, so ITP writes never look at a packet's status. Default: a filed packet does not lock the ITP. This resolves the phase 5 follow-up. Applied in phase 6. Changed by SethD on 2026-10-26, Gitea issue 25 (change C3 below). Built on 2026-10-30: ITP writes still never look at a packet, but FILE waits until the unit's ITP is complete.
29. ITP status: the 0003 enum had `open`, `signed` and `reopened`. Default: `itp_status` gains `complete` for a signed-off ITP; a reopened ITP is `open` again with the reopen recorded. `signed` and `reopened` stay in the enum unused, since PostgreSQL cannot drop an enum value. Applied in phase 6.
30. Removing an ITP block signature: the old build let any Q.C. editor undo it. Default: the signer or an admin, while the ITP is open. The block's QN, remarks and customer fields stay. Applied in phase 6. Confirmed by SethD on 2026-10-26, Gitea issue 26: a fix to customer fields or an earlier step's remarks goes back to that step or to Clerical.
31. The signed ITP PDF on sign-off: default a hook (`pdf.itp.file_signed_itp_pdf`) that phase 7 builds. Today it files nothing, the sign-off log says `PDF filing reserved for phase 7`, and the automatic `qc_docs` ITP row is written only when the hook returns a file. Applied in phase 6. Phase 7 (2026-10-17) built the hook: the sign-off files the signed ITP PDF and the automatic ITP document in the same transaction, and a PDF that cannot be built fails the sign-off.
From phase 7 (2026-10-17), for Seth and Clerical to veto:
32. Prints: the old build pushed page images into the browser's print dialog and offered the same PDF when the dialog was blocked. Default: a print opens as a stamped PDF in a new browser tab, and the person prints it from the PDF viewer (Ctrl+P). One path is simpler and prints the same stamped pages. Phase 7 (2026-10-17) applied the default. Changed by SethD on 2026-10-26, Gitea issue 27 (change C4 below).
32. Prints: the old build pushed page images into the browser's print dialog and offered the same PDF when the dialog was blocked. Default: a print opens as a stamped PDF in a new browser tab, and the person prints it from the PDF viewer (Ctrl+P). One path is simpler and prints the same stamped pages. Phase 7 (2026-10-17) applied the default. Changed by SethD on 2026-10-26, Gitea issue 27 (change C4 below). Built on 2026-10-30.
33. When the data book is built: default on the server when the packet is filed, inside the filing transaction, stored as an asset on the seal. A data book that cannot be built fails the filing, so nothing is filed half way. Before filing, the data book can be viewed or downloaded at any time from status complete on; that copy is stamped "not yet filed". Phase 7 (2026-10-17) applied the default. Confirmed by SethD on 2026-10-26, Gitea issue 28.
34. Who may read the PDFs and the CSV: default every signed-in role that can open a packet, PM included. Every print and every export is logged with who, when, what and how many pages, in the append-only `print_log` and in the audit. Phase 7 (2026-10-17) applied the default. Confirmed by SethD on 2026-10-26, Gitea issue 29: everyone may view the whole packet; only the holder edits, and missing work goes back to the department that owes it.
35. The old "Original packet PDF" print kind: the rebuild has no VISUAL packet PDF. The drawing pages come from M-Files or a hand upload, and "Drawing (no markup)" prints them as attached. Default: the kind is gone. Phase 7 (2026-10-17) applied the default. Confirmed by SethD on 2026-10-28, Gitea issue 30: nothing is missed.
36. Filing while the feed still shows a work order open in VISUAL: default a second confirmation (`confirm_open_wos`), a warning and not a block, on both routes. The work orders are kept on the seal and in the log note. A hand packet has no feed state and no warning. Phase 7 (2026-10-17) applied the default. Changed by SethD on 2026-10-26, Gitea issue 31 (change C5 below).
37. The controlled-copy stamp carries a short signature made with the seal key, so a printed or exported copy can be checked against the server later. Default: phase 7 ships the signature, the check function and the print log; phase 9 adds the Admin screen that checks a copy. Phase 7 (2026-10-17) applied the default. Changed by SethD on 2026-10-28, Gitea issue 32 (change C9).
36. Filing while the feed still shows a work order open in VISUAL: default a second confirmation (`confirm_open_wos`), a warning and not a block, on both routes. The work orders are kept on the seal and in the log note. A hand packet has no feed state and no warning. Phase 7 (2026-10-17) applied the default. Changed by SethD on 2026-10-26, Gitea issue 31 (change C5 below). Built on 2026-10-30: `confirm_open_wos` is gone and filing is blocked. Two follow-up questions were asked on issue 31 on 2026-10-30 (see C5).
37. The controlled-copy stamp carries a short signature made with the seal key, so a printed or exported copy can be checked against the server later. Default: phase 7 ships the signature, the check function and the print log; phase 9 adds the Admin screen that checks a copy. Phase 7 (2026-10-17) applied the default. Changed by SethD on 2026-10-28, Gitea issue 32 (change C9). Built on 2026-10-30.
From phase 8 (2026-10-18), for Seth to veto. The phase 8 design numbered these 35 to 39; 35 to 37 were already taken by phase 7, so they follow on from 38.
38. Hours vs target: the station timer stays the clock of record. "Over target" means timer hours over the work-order target. VISUAL ticket hours are shown beside the timer in every hours table and never decide the flag. Phase 8 (2026-10-18) applied the default. Changed by SethD on 2026-10-26, Gitea issue 1 (change C1 below).
39. PM exports and who reads the reports: every PM export is logged as `report exported` in the audit and in `print_log`, with the scope and the period. Default: any signed-in role may read the PM reports (as every role may open every packet), not only PS-ProjectMgmt. Phase 8 (2026-10-18) applied the default. Changed by SethD on 2026-10-28, Gitea issue 33 (change C10).
38. Hours vs target: the station timer stays the clock of record. "Over target" means timer hours over the work-order target. VISUAL ticket hours are shown beside the timer in every hours table and never decide the flag. Phase 8 (2026-10-18) applied the default. Changed by SethD on 2026-10-26, Gitea issue 1 (change C1 below). Built on 2026-10-30.
39. PM exports and who reads the reports: every PM export is logged as `report exported` in the audit and in `print_log`, with the scope and the period. Default: any signed-in role may read the PM reports (as every role may open every packet), not only PS-ProjectMgmt. Phase 8 (2026-10-18) applied the default. Changed by SethD on 2026-10-28, Gitea issue 33 (change C10). Built on 2026-10-30.
40. Presence shows the display name and the role (the screen the person has open). Default: locked sessions drop out of presence at once; closing the tab drops out within the stream's ping cycle (25 seconds). Phase 8 (2026-10-18) applied the default. Confirmed by SethD on 2026-10-28, Gitea issue 34.
41. The find bar filters are applied by the server on every board and list, so a board never fetches more than it shows. Default: the KPI numbers follow the project pick only, not the other filters. Phase 8 (2026-10-18) applied the default. Confirmed by SethD on 2026-10-28, Gitea issue 35.
42. Mine is decided from the person's AD roles and the screen they are on. Default: the stall's Mine needs the picked station and shows nothing without one. Phase 8 (2026-10-18) applied the default. Changed by SethD on 2026-10-28, Gitea issue 36 (change C11).
42. Mine is decided from the person's AD roles and the screen they are on. Default: the stall's Mine needs the picked station and shows nothing without one. Phase 8 (2026-10-18) applied the default. Changed by SethD on 2026-10-28, Gitea issue 36 (change C11). Built on 2026-10-30.
From phase 9 (2026-10-21), for Seth or Spencer to veto:
@@ -289,24 +290,31 @@ From phase 9 (2026-10-21), for Seth or Spencer to veto:
49. New departments: default only shop departments can be added in the app (422 `support_fixed` for a support department). The support roles are fixed in the `role_kind` enum and the group map, so a new support department would map to no role. Phase 9 (2026-10-21) applied the default.
50. The profile CSV `stations` cell: default the cell is the person's whole list of station limits across departments and replaces every limit they had; `-` clears them all; an empty cell leaves them. Phase 9 (2026-10-21) applied the default.
51. A direct welder assign: default it does not check `profile_active`, so an Admin's inactive switch hides a person from the picker but a typed assign still works. Phase 9 (2026-10-21) applied the default; say if an inactive profile should be refused there too.
52. A checklist item added while a stage is open: default it applies to that open stage too, because completeness reads the live template, so the item must be ticked before submit. Phase 9 (2026-10-21) applied the default. Replaced by change C8: a template change applies to new packets only.
52. A checklist item added while a stage is open: default it applies to that open stage too, because completeness reads the live template, so the item must be ticked before submit. Phase 9 (2026-10-21) applied the default. Replaced by change C8: a template change applies to new packets only. Built on 2026-10-30: a stage freezes its list at START, so a change reaches only stages not yet started.
Changes decided by SethD on 2026-10-26, to build (phase 10 unless a later plan moves them). Each replaces a default above:
Changes decided by SethD on 2026-10-26. Each replaces a default above. All five were built on 2026-10-30 (branch `seth-changes`):
- C1 (issue 1, Q1 and Q38): the station timer stays and is shown as today, but "over target" is decided by the VISUAL ticket hours against the work-order target, not by the timer.
- C2 (issue 24, Q27): a Q.C. reject always needs a detailed reason; an NCR number no longer stands in for it. A rejected item cannot be accepted until an NCR number is entered. Q.C. can upload the NCR document to the packet, and it goes into the data book.
- C3 (issue 25, Q28): a packet whose unit has an ITP that is not complete cannot be filed, so no data book is built. The packet goes back to have the ITP completed.
- C4 (issue 27, Q32): prints go straight to the browser's print dialog from the page, as the old build did, with no new tab. The stamped pages stay the same.
- C5 (issue 31, Q36): filing is blocked, not warned, while VISUAL still shows a work order open. The packet waits in a Clerical "Ready to close" column until the feed sees VISUAL closed, then moves to "Ready for data book". This applies to both routes unless Seth says Shipping is different.
- C1 (issue 1, Q1 and Q38): the station timer stays and is shown as today, but "over target" is decided by the VISUAL ticket hours against the work-order target, not by the timer. Built 2026-10-30: `domain/pm.py` `is_over()` flags a packet when its ticket hours are above a target above zero, so a packet with no ticket hours is never over. The hours tables, the "Packets over target" table, the KPI, the summary line, the CSV and the report PDF follow, with columns named "Ticket h / target" and "Over target (tickets)". The timer column stays.
- C2 (issue 24, Q27): a Q.C. reject always needs a detailed reason; an NCR number no longer stands in for it. A rejected item cannot be accepted until an NCR number is entered. Q.C. can upload the NCR document to the packet, and it goes into the data book. Built 2026-10-30: QC_REJECT needs a reason of 8 characters and may also carry an NCR number. When a reject since the last accept has no NCR number, QC_ACCEPT and QC_ACCEPT_AND_ROUTE need one in the request (precondition `ncr_given`, 422 `ncr_required`); the number is stored on the accepting inspection and in the log. The packet read has `ncr_required`, and the Q.C. bar shows the NCR field as required. An `ncr` packet document is in section 5 of the data book.
- C3 (issue 25, Q28): a packet whose unit has an ITP that is not complete cannot be filed, so no data book is built. The packet goes back to have the ITP completed. Built 2026-10-30: precondition `itp_complete` on FILE. An ITP that is not complete (open, signed or reopened) blocks; a unit with no ITP does not. The reason shows under File in the close panel.
- C4 (issue 27, Q32): prints go straight to the browser's print dialog from the page, as the old build did, with no new tab. The stamped pages stay the same. Built 2026-10-30: every print in the app goes through `frontend/src/lib/printPdf.ts`, which renders the stamped PDF with pdf.js into a hidden print area and opens the print dialog. Viewing a PDF (data book, traveler, ITP PDF, PM report) still opens a tab. The server did not change.
- C5 (issue 31, Q36): filing is blocked, not warned, while VISUAL still shows a work order open. The packet waits in a Clerical "Ready to close" column until the feed sees VISUAL closed, then moves to "Ready for data book". This applies to both routes unless Seth says Shipping is different. Built 2026-10-30: precondition `closed_in_visual` replaces `vclosed_if_databook`. On a VISUAL packet the feed is the one writer of `vclosed_at`: it sets it when every leg is closed or cancelled and clears it when a leg shows open again, so the Clerical board moves the packet by itself. FILE needs it and no open leg, on both routes. A hand packet has no feed state, so Clerical still confirms the close by hand (SET_VCLOSED, Data Book route only); SET_VCLOSED is refused on a VISUAL packet. `confirm_open_wos` and the 409 `open_work_orders` are gone.
Asked on issue 31 on 2026-10-30, open until answered: (a) a work order deleted from VISUAL while open keeps its last status, so its packet can never file. Should a leg gone for 2 runs count as closed, or should an Admin close it with a reason? (b) Shipping packets now wait for the VISUAL close too. Is that right for Shipping, or should Shipping file without it? Until Seth answers, both stay as built.
Changes decided by SethD on 2026-10-28, to build in phase 10:
Changes decided by SethD on 2026-10-28. All six were built on 2026-10-30 (branch `seth-changes`):
- C6 (issue 8, Q9, D7): the data book leaves out the packet history pages. It is the one copy, and it goes to the customer. History stays readable in Packet Station.
- C7 (issue 18, Q21): every received short also needs the department supervisor's approval, one short at a time. The supervisor checks the part and its heat number against what the warehouse issued. A packet cannot be marked complete until every short is received and approved.
- C8 (issue 23, Q26 and Q52): checklists become per department. Each department supervisor edits their own department's checklist, Admin edits any, and Q.C. does not edit department checklists. A change applies to packets not yet started, never to packets already started.
- C9 (issue 32, Q37): the stamp check is open to Admin, Q.C. and Clerical.
- C10 (issue 33, Q39): the PM screen and its reports are for PS-ProjectMgmt and Admin only.
- C11 (issue 36, Q42): the Stall screen shows a welder the packets assigned to him, each at the stall the supervisor picked when assigning. The welder does not pick a stall. Moving a welder to another stall is a re-assign by the supervisor.
- C6 (issue 8, Q9, D7): the data book leaves out the packet history pages. It is the one copy, and it goes to the customer. History stays readable in Packet Station. Built 2026-10-30: section 7 "Record history" is gone and the data book has six sections. The change-order table it held moved to a "Change order record" page at the end of section 1, only when the packet has change orders. Say if that page should go too.
- C7 (issue 18, Q21): every received short also needs the department supervisor's approval, one short at a time. The supervisor checks the part and its heat number against what the warehouse issued. A packet cannot be marked complete until every short is received and approved. Built 2026-10-30: migration 0011 adds `short_approvals` (append-only, one row per receipt). New action SHORT_APPROVE: the supervisor of the packet's current department (a lead is not enough), Admin through the bypass, any status but filed, signature "Approve a received short", at `POST /api/packets/{id}/shorts/receipts/{receipt_id}/approve`. Precondition `shorts_approved` on QC_ACCEPT and FILE, not on QC_ACCEPT_AND_ROUTE. The Materials tab shows each receipt with who received it and who approved it. There is no list of receipts to approve on the supervisor board yet.
- C8 (issue 23, Q26 and Q52): checklists become per department. Each department supervisor edits their own department's checklist, Admin edits any, and Q.C. does not edit department checklists. A change applies to packets not yet started, never to packets already started. Built 2026-10-30: a department's own list is the settings row `checklist:<code>`; without one it uses the default for its kind (the weld or plain list in `checklist_templates`). `GET /api/checklists`, `GET` and `PUT /api/checklists/{dept}` with version compare-and-set. The department's Supervisor (not a lead) and Admin may save, in a password session. `packet_stages.checklist_template` (migration 0011) freezes the list at START, and every reader uses the frozen list when set. New screen `/checklists`.
- C9 (issue 32, Q37): the stamp check is open to Admin, Q.C. and Clerical. Built 2026-10-30: `POST /api/stamp-verify` and `POST /api/stamp-verify/pdf` for Admin and every Q.C. and Clerical level, in a password session. The `/api/admin` paths stay as aliases. New screen `/stamp-check`.
- C10 (issue 33, Q39): the PM screen and its reports are for PS-ProjectMgmt and Admin only. Built 2026-10-30: every `/api/pm/*` endpoint is 403 for anyone else. No other screen calls `/api/pm`. The PM screen entry shows only for those roles.
- C11 (issue 36, Q42): the Stall screen shows a welder the packets assigned to him, each at the stall the supervisor picked when assigning. The welder does not pick a stall. Moving a welder to another stall is a re-assign by the supervisor. Built 2026-10-30: `GET /api/stall/mine` lists the packets assigned to the person (assigned, in progress, in review), each with the station the supervisor picked, across stations. The Stall screen has no station picker, and the stall's Mine no longer needs a station. Someone with nothing assigned sees "Your supervisor assigns packets to you."
Known edges of C1 to C11 at deploy:
- Packets in flight. Every short receipt recorded before the deploy has no approval, so the supervisor approves it before the next Q.C. accept, or before filing a packet that is already complete. A Q.C. reject since the last accept that has no NCR number needs one at the next accept. Nothing is backfilled.
- Stages started before migration 0011. The migration freezes the open stage of every packet in progress, in review or at Q.C. to the default list for its kind, from the `checklist_templates` row. A department edit does not reach those stages. When that row is missing, such a stage has no frozen list and reads its department's live list until it closes. A packet whose stages all closed before 0011 also reads the live list, so a later edit changes what its checklist shows.
- The feed sets `vclosed_at` in any status before filed, so a packet can come back from Q.C. straight into "Ready for data book".
## Verification
+98 -43
View File
@@ -90,9 +90,9 @@ Statuses in workflow order: draft, warehouse, ready, assigned, in_progress, revi
| send_to_qc | DEPT_SUP | review | qc | no | yes | qc_book_spool_location | Send to Q.C. | |
| route_from_review | DEPT_SUP | review | ready | required | yes | target_dept_given | Route to another department | |
| qc_reject | QC | qc | review | no | | reason_given | Q.C. reject | yes |
| qc_accept | QC | qc | complete | no | yes | qc_missing_empty, route_given | Q.C. accept | |
| qc_accept_and_route | QC | qc | ready | required | yes | qc_missing_empty, target_dept_given | Q.C. accept and route | |
| file | CL | complete | filed | no | yes | vclosed_if_databook | File in the data book | |
| qc_accept | QC | qc | complete | no | yes | qc_missing_empty, shorts_approved, route_given, ncr_given | Q.C. accept | |
| qc_accept_and_route | QC | qc | ready | required | yes | qc_missing_empty, target_dept_given, ncr_given | Q.C. accept and route | |
| file | CL | complete | filed | no | yes | closed_in_visual, itp_complete, shorts_approved | File in the data book | |
| admin_reopen | ADMIN | filed (filed_ok) | complete | no | | reason_given | Reopen a filed packet | yes |
| edit_header | CL | draft | | | | | | |
| edit_drawing_ref | CL | any | | | | | | |
@@ -101,6 +101,7 @@ Statuses in workflow order: draft, warehouse, ready, assigned, in_progress, revi
| edit_wos | CL | draft | | | | | | |
| edit_bom | HOLDER | draft, warehouse, SHOP | | | | | | |
| wh_receive_short | WH | any | | | | short_open | | |
| short_approve | DEPT_SUP | any | | | | | Approve a received short | |
| edit_heats | HOLDER | warehouse, SHOP | | | | | | |
| edit_heat_table | HOLDER | warehouse, SHOP | | | | | | |
| edit_welds | HOLDER | SHOP | | | | | | |
@@ -113,7 +114,7 @@ Statuses in workflow order: draft, warehouse, ready, assigned, in_progress, revi
| doc_add | CL_OR_QC | any | | | | | | |
| doc_remove | CL_OR_QC | any | | | | | | |
| rev_approve | CL_SUP | draft | | | | reason_given | Approve the M-Files revision | yes |
| set_vclosed | CL | complete, only when routed_to is databook | | | | | | |
| set_vclosed | CL | complete, only for a hand packet routed to the data book | | | | | | |
| set_databook_at | CL_OR_QC | any (filed_ok) | | | | | | |
| remove | CL | draft | | | | reason_given | | yes |
| co_issue | CL | any | | | | | Issue a change order | |
@@ -141,7 +142,11 @@ The `vis_*` actions are system only. People can never do them, and the worker ca
The Q.C. folder and ITP actions ("no packet (scoped)") belong to a project or a unit, not to a packet. They are checked with `check_scoped()` (see "Scoped checks"), never with the packet status, custody or the filed lock.
qc_reject takes an NCR number in place of the reason text (see "Q.C. (phase 6)", "Reject").
qc_reject always needs a reason of 8 characters and may also carry an NCR number. After a reject with no NCR number, qc_accept and qc_accept_and_route carry one (Gitea issue 24, change C2; see "Q.C. (phase 6)", "Reject").
short_approve is the department supervisor's approval of one received short (Gitea issue 18, change C7; see "Supervisor approval of received shorts"). A lead is not enough. The filed lock applies, so it is open in any status but filed.
set_vclosed is for hand packets only. On a VISUAL packet the feed sets the close (Gitea issue 31, change C5; see "Close in VISUAL").
## Preconditions
@@ -161,29 +166,34 @@ Each precondition is a named predicate over the packet state and the request inp
| qc_book_spool_location (input) | book and spool confirmed and a location given | Confirm the book and the spool, and give the location. |
| qc_missing_empty | the Q.C. record has nothing missing | Q.C. still needs: (the list). |
| reason_given (input) | a reason of at least 8 characters after cleaning | Give a reason of at least 8 characters. |
| vclosed_if_databook | routed to Shipping, or closed in VISUAL | Close the job in VISUAL first. |
| closed_in_visual | VISUAL packet, either route: the feed shows no work order open and has set `vclosed`. Hand packet: routed to Shipping, or Clerical confirmed the close | Hand packet: Close the job in VISUAL first. VISUAL packet: VISUAL still shows work orders open on this packet: L1/001, L2/7A. Filing waits until VISUAL closes them. / Filing waits until the VISUAL feed sees the job closed. |
| itp_complete | the packet's unit has no ITP, or its ITP is complete | Complete the unit's ITP first. Q.C. signs it off on the ITP page. |
| shorts_approved | no open short, and every short receipt has the supervisor's approval | Every short must be received and approved by the supervisor first. |
| ncr_given (input) | no Q.C. reject since the last accept lacks an NCR number, or the request gives a well-formed `ncr` | Enter the NCR number of the reject first. |
| co_staged_if_dwg_change (input) | no drawing change, or the new drawing is staged (phase 3) | Stage the new drawing for the change order first. |
| route_given (input) | routed_to is databook or shipping | Choose Data Book or Shipping. |
| target_dept_given (input) | a shop department different from the packet's; from a draft any shop department | Choose a department. / Choose a different department. |
"(input)" marks a precondition that reads request input, not packet state. `can()` skips those.
"(input)" marks a precondition that reads request input, not packet state. `can()` skips those. `ncr_given` reads the state too (`ncr_required`), but the user gives the number with the step, so `can()` skips it and the packet read carries `ncr_required` for the screen.
closed_in_visual replaced vclosed_if_databook (Gitea issue 31, change C5). itp_complete is Gitea issue 25 (change C3). shorts_approved is Gitea issue 18 (change C7). ncr_given is Gitea issue 24 (change C2). All built 2026-10-30. A FILE that fails several shows every reason in `blocked.file`.
## Order of checks
`check(principal, state, action, *, extra={}, it_reason=None) -> Decision` runs these in this order and stops at the first failure:
1. **Removed.** A removed packet refuses everything, Admin included. Code `removed`.
2. **Status.** The status must be in the rule's from set. RETURN_TO_CLERICAL also needs wh_skipped. SET_VCLOSED also needs routed_to databook. An unknown status fails here too. Code `wrong_status`.
2. **Status.** The status must be in the rule's from set. RETURN_TO_CLERICAL also needs wh_skipped. SET_VCLOSED also needs a hand packet routed to the data book; on a VISUAL packet it fails with "The VISUAL feed marks this job closed when it sees every work order closed." An unknown status fails here too. Code `wrong_status`.
3. **Change-order hold.** A held action refuses while an approved change order waits for verification, or a pending one is on hold. Admin included. Code `co_hold`, with the hold text.
4. **System.** A system-only action refuses every person (`system_only`). The system principal passes only system-only actions (`forbidden`).
5. **Admin bypass.** An admin who is not allowed by the rule's Who passes who and custody when an IT reason of at least 8 characters is given. Without one the code is `it_reason_required`. SET_DATABOOK_AT needs no reason. The decision is marked `admin_bypass`, and the write marks the log row as an IT edit.
6. **Filed lock.** A filed packet refuses everything except SET_DATABOOK_AT and ADMIN_REOPEN, Admin included. Code `filed`: "This packet is filed and locked. IT can reopen it if a correction is needed."
7. **Who and custody.** Code `forbidden`. For a step: "Your access doesn't allow this step." For an edit: "Your access doesn't allow changing (what) on this packet. It's with (holder) right now." PM fails here for everything.
8. **Preconditions,** in rule order, never bypassed. Code `precondition` with the message, or `reason_required` when the missing piece is the rule's reason, so the screen can ask for one.
8. **Preconditions,** in rule order, never bypassed. Code `precondition` with the message, `reason_required` when the missing piece is the rule's reason, so the screen can ask for one, or `ncr_required` when `ncr_given` fails.
`Decision` carries `ok`, `code`, `why`, `admin_bypass`, `signature` (the meaning text when the rule needs one) and, in read mode only, `reasons`.
The packet API answers 403 with the code for most failures, and 400 for `it_reason_required` and `reason_required`.
The packet API answers 403 with the code for most failures, 400 for `it_reason_required` and `reason_required`, and 422 for `ncr_required`.
### Scoped checks
@@ -198,7 +208,7 @@ There is no removed, status, change-order hold, filed lock or custody step: ther
## Signatures
These actions need a signature: submit, send_to_qc, route_from_review, qc_reject, qc_accept, qc_accept_and_route, file, admin_reopen, edit_document_set (attach only), rev_approve, co_issue, co_approve, co_reject, co_verify, itp_block_sign and itp_sign_off. The rule's signature text is the meaning that is signed.
These actions need a signature: submit, send_to_qc, route_from_review, qc_reject, qc_accept, qc_accept_and_route, file, admin_reopen, short_approve, edit_document_set (attach only), rev_approve, co_issue, co_approve, co_reject, co_verify, itp_block_sign and itp_sign_off. The rule's signature text is the meaning that is signed.
The request carries `sign: {factor, secret}`. The write pipeline calls the phase 1 helper `packetstation.auth.signatures.sign()` inside the same transaction, after the policy passes. It checks the PIN or domain password against the session's own user. A PIN works in any session. It logs the attempt and applies the five-strike PIN disable and the three-strike session end. It writes an append-only `signatures` row with the user, session, account, display name, session kind, factor, action, meaning, packet, subject and a sha256 over the canonical JSON of what was signed (the action, the packet id, the expected state and the request fields). The `packet_transitions` and `packet_log` rows point at it.
@@ -211,7 +221,7 @@ The request carries `sign: {factor, secret}`. The write pipeline calls the phase
- **Work orders** are a draft-only full replace with If-Match on the packet version.
- **Shorts and change-order actions** are state compare-and-set. Checklist keys are written one key at a time. The heat table position is an idempotent upsert. A change-order verification is an insert, so a second one is "already verified".
- **ITP lines** are approved under the `itps` row lock, with a state check and an optional `If-Match: <row_version>`. ITP header fields use the same `set` and `base` field compare-and-set as packet headers.
- **Append-only tables** never conflict: packet_transitions, packet_log, qc_history, short_receipts, project_renumbers, itp_rejects, itp_log, print_log and co_verifications. Triggers refuse UPDATE, DELETE and TRUNCATE, and the app role has no grant for them.
- **Append-only tables** never conflict: packet_transitions, packet_log, qc_history, short_receipts, short_approvals, project_renumbers, itp_rejects, itp_log, print_log and co_verifications. Triggers refuse UPDATE, DELETE and TRUNCATE, and the app role has no grant for them.
## Reads: can, blocked and holder
@@ -283,6 +293,16 @@ Receiving short parts (`POST .../bom/{item}/short/receipts`) picks its action fr
The warehouse still cannot change any other field of a packet it no longer holds. EDIT_BOM stays the holder's. A receipt needs a heat unless the line is N/A, and its quantity is 1 to what is still open.
### Supervisor approval of received shorts
Built 2026-10-30 (Gitea issue 18, change C7). Every receipt also needs the approval of the department supervisor, who checks the part and its heat number against what the warehouse issued. One approval per receipt.
- SHORT_APPROVE: the supervisor of the packet's current department (DEPT_SUP; a lead is not enough), or Admin through the bypass with an IT reason. Any status but filed. Signature "Approve a received short".
- `POST /api/packets/{id}/shorts/receipts/{receipt_id}/approve` `{sign, note?, it_reason?}` answers the packet read. A receipt not on the packet is 404 `not_found`. A second approval is 409 `already_approved` "This receipt is already approved.", checked before the signature and again under the packet lock; the unique key on `short_approvals.receipt_id` backs it.
- The approval row keeps the department the packet was in. After a QC_ACCEPT_AND_ROUTE the new department's supervisor approves the earlier receipts.
- `shorts_unapproved` (packet read, board cards and the policy state) counts open shorts plus receipts without an approval. The precondition `shorts_approved` needs it at 0 on QC_ACCEPT and FILE. QC_ACCEPT_AND_ROUTE does not need it, because the packet goes on to another department and is not complete.
- The packet read has `shorts`: every short of the packet, open or closed, each with its `receipts` and each receipt's `approval` (`null`, or `{at, by, note, dept_code}`). A short that is fully received closes and leaves the BOM line, but its receipts still need approval, so the screen reads `shorts`. `can` holds `short_approve` for the supervisor in every status but filed; the screen shows Approve only on a receipt without an approval.
### MTR files
The MTR on a line must be an asset whose type is PDF, PNG or JPEG. Anything else, or an asset that is not there, is 422 `invalid_asset`. Clearing the MTR is allowed.
@@ -307,6 +327,7 @@ The `action` column of `packet_log` for each warehouse write. `{item}` is the BO
| Short opened | `Short recorded · item {item}` | `{qty} of {need} short · {reason} · {ref} · ETA {eta}`, parts present only |
| Short edited | `Short updated · item {item}` | the same as recorded |
| Not short after all | `Short cleared · item {item}` | |
| Short approved | `Short approved · item {item}` | `{qty} pc · heat {heat} · {note}`, parts present only |
| Short received | `Short received · item {item}` | `{qty} pc · heat {heat} · {still} still short` or `... · short closed`, then ` · packet {status}` (lower case) and ` in {station}` when at a station |
| Stage incoming to pulling | `Warehouse: Started pulling` | |
| Stage to hold | `Warehouse: Put on hold` | `{reason} · {note}` |
@@ -359,7 +380,7 @@ A lead holds a packet in review (see "Custody"), but return_to_station, send_to_
| Step | Precondition | Passes when | Message |
|---|---|---|---|
| assign | welder_set (input) | the request gives station_id and welder_id | Pick a station and a welder. |
| submit | checklist_complete | every key of the department's checklist is done | Finish the checklist first. |
| submit | checklist_complete | every key of the stage's checklist is done: the list frozen at START, else the department's live list (see "Checklists") | Finish the checklist first. |
| submit | welds_recorded | at least one live weld whose `dept_code` is the packet's current department, in a weld department | Record at least one weld first. |
| submit | filler_set | filler metal entered on the open stage, in a weld department | Enter the filler metal first. |
| send_to_qc | qc_book_spool_location (input) | book and spool confirmed and a location given | Confirm the book and the spool, and give the location. |
@@ -410,8 +431,9 @@ The sibling is a live packet in the same unit, with a different id and the same
### Shop screens
- `GET /api/shop/board` covers the shop departments where the person is lead or supervisor. Admin sees every active shop department. A department outside that scope is 403 `forbidden`. A person who leads no department gets 403 `forbidden` "You do not lead a department." The change order list also shows open change orders anywhere that wait on one of the person's departments.
- `GET /api/shop/stations` lists the stations the person may pick for the Stall screen: every station of their shop departments at any level, cut to their station limits for a worker. Admin gets every station.
- `GET /api/shop/stall?station_id=` is 403 for a station not in that list. Admin passes.
- `GET /api/stall/mine` is the Stall screen (Gitea issue 36, change C11, built 2026-10-30). It lists the packets assigned to the signed-in person (`welder_id` is them; status assigned, in progress or review), each at the station the supervisor picked, across stations. Nobody picks a station on the Stall screen, and the request names none: the session decides. The answer is `{station: null, mine, submitted, facets, empty}`; `mine` holds the assigned and in-progress cards, `submitted` the cards in review, and every card carries `station_id` and `station_name`. With nothing assigned, `empty` says "Nothing is assigned to you now. Your supervisor assigns packets to you." The find bar narrows the cards; a `station_id` in the query narrows, never widens. Moving a welder to another station is a re-assign by the supervisor.
- `GET /api/shop/stations` lists the stations of the person's shop departments, cut to their station limits for a worker. Admin gets every station. The Stall screen no longer uses it.
- `GET /api/shop/stall?station_id=` is the board of one station, 403 for a station not in that list. Admin passes. It stays for now; the Stall screen no longer uses it.
### Presence
@@ -470,7 +492,7 @@ Q.C. holds a packet in status qc. EDIT_QC (QC, status qc) covers every write to
### Accept
Two actions accept. Both are Q.C. from status qc, held by an open change order, gated by `qc_missing_empty`, and signed.
Two actions accept. Both are Q.C. from status qc, held by an open change order, gated by `qc_missing_empty` and `ncr_given`, and signed. qc_accept, which marks the packet complete, also needs `shorts_approved` (Gitea issue 18, change C7): no open short and every receipt approved by the supervisor. qc_accept_and_route does not, because the packet goes on to another department.
| Route | Action | Body | Result |
|---|---|---|---|
@@ -480,6 +502,8 @@ Two actions accept. Both are Q.C. from status qc, held by an open change order,
The signature meanings are "Q.C. accept" and "Q.C. accept and route". The accept closes the inspection into `qc_history` and closes the stage with the inspection as its `qc_snapshot`. The note in the log counts every live weld of the packet.
NCR on the accept (Gitea issue 24, change C2, built 2026-10-30). The packet read's `ncr_required` is true when a Q.C. reject after the packet's last Q.C. accept has no NCR number (a `qc_history` row with result `rejected` and no `snapshot.ncr`). Then both accepts need `ncr` in the request: without it, or with a blank one, the answer is 422 `ncr_required` "Enter the NCR number of the reject first." A malformed one is 422 `invalid_ncr`. The number goes through `validate_ncr`, is stored on the accepting inspection (`qc_inspections.ncr`, so `qc_last` and the `qc_history` snapshot show it) and is added to the log note. An NCR given on an accept that does not need one is stored the same way. `can()` skips `ncr_given`, since the number comes with the step; the screen shows the NCR field as required from `ncr_required`.
### What Q.C. still needs
`qc_missing_empty` reads the list built by `checklists.qc_missing()`. Its message is `Q.C. still needs: ` followed by the parts joined by `, ` and a full stop. `blocked.qc_accept` and `blocked.qc_accept_and_route` carry it, so the screen shows Accept disabled with the reason.
@@ -494,11 +518,14 @@ The signature meanings are "Q.C. accept" and "Q.C. accept and route". The accept
### Reject
qc_reject is Q.C. from status qc back to review, signed ("Q.C. reject"). It takes a reason, an NCR number, or both:
qc_reject is Q.C. from status qc back to review, signed ("Q.C. reject"). It always needs a detailed reason and may also carry an NCR number (Gitea issue 24, change C2, built 2026-10-30):
- An NCR number goes through `validate_ncr`: trimmed, upper case, then the pattern `NCR-` four digits `-` four digits. A bad one is 422 `invalid_ncr` "Enter an NCR number like NCR-2026-0021."
- The reason checked by `reason_given` is the NCR alone (`NCR-2026-0021`) when no text is given, else `NCR-2026-0021 · <reason>`. So an NCR number stands in for the reason, and the text has no minimum when an NCR is given.
- With neither, the answer is 400 `reason_required` with the rule's message. A reason without an NCR needs 8 characters.
- The reason must pass `reason_given`: at least 8 characters after cleaning. An NCR number no longer stands in for it. Without one the answer is 400 `reason_required` with the rule's message.
- An NCR number, when given, goes through `validate_ncr`: trimmed, upper case, then the pattern `NCR-` four digits `-` four digits. A bad one is 422 `invalid_ncr` "Enter an NCR number like NCR-2026-0021."
- With both, the return note and the result note read `NCR-2026-0021 · <reason>`.
- A reject without an NCR number means the next accept must carry one (see "Accept").
Q.C. can add the NCR document to the packet as kind `ncr` (see "Packet documents"). It goes into section 5 of the data book.
The reject stores the NCR on the inspection, sets the packet's `return_note` to the reason, and closes the inspection into `qc_history` at once. The packet in review has no open inspection; the read model gives the reject as `qc_last`, and the screens show the "Q.C. rejected" callout from it. The supervisor's next send to Q.C. opens a fresh inspection.
@@ -596,6 +623,7 @@ The `action` and `note` of `packet_log` for each Q.C. packet write.
| qc_accept to the data book | `Q.C. accepted → Clerical – Data Book` | `Quinn QC [Q1] · 3 welds VT accepted`, plus ` · RT Acceptable` when an NDE method other than None required is set, plus ` · <note>` when the step carries a note |
| qc_accept to Shipping | `Q.C. accepted → Shipping` | the same |
| qc_accept_and_route | `Q.C. accepted → 09 Hydro` | `Quinn QC [Q1] · from 03 Weld Parts` |
| either accept, with an NCR number | as above | the note above plus ` · NCR-2026-0021` |
| qc_reject | `Q.C. rejected → supervisor` | `NCR-2026-0021 · <reason> · welds W1, W3` (parts present only; the rejected welds by number) |
| Document added | `Document added` | `<label> · <title> · <file name>` |
| Document taken out | `Document taken out of the data book` | `<title> · <reason>` |
@@ -625,19 +653,26 @@ A Q.C. folder document writes an audit row `qc_doc.add` or `qc_doc.revise` (subj
## Phase 7: close, file, PDFs and prints
Nothing in the rule table changes in phase 7. FILE, ADMIN_REOPEN, SET_VCLOSED and SET_DATABOOK_AT keep their rows.
Nothing in the rule table changes in phase 7. FILE, ADMIN_REOPEN, SET_VCLOSED and SET_DATABOOK_AT keep their rows. Changes C2, C3, C5 and C7 (built 2026-10-30) changed FILE and SET_VCLOSED, as below.
### Close in VISUAL
SET_VCLOSED is Clerical, in status complete, only when the packet is routed to the data book (`POST /api/packets/{id}/vclosed` `{closed}`). Confirming sets `vclosed_at` and `vclosed_by`. Taking it back clears both ("Not closed in VISUAL, move back"). A packet routed to Shipping files without it.
`packets.vclosed_at` is the one record of "closed in VISUAL" (Gitea issue 31, change C5, built 2026-10-30). Who writes it depends on the packet:
The packet read carries `vclose_check`, outside the seal. It is null for a hand packet. For a feed packet: `observed_at` and `closed_at` from the feed's VISUAL state, `open_wos` (the legs whose status is not `C` and not cancelled, as `wo_base/lot` text, never cast), and `agrees` (true when the Clerical confirm and the feed's closed state say the same). The feed state is a hint. Clerical's confirm is the record, and the precondition `vclosed_if_databook` reads only the confirm.
- A VISUAL packet: the feed alone. It sets `vclosed_at` (with `vclosed_by` null) when every leg is closed or cancelled, and clears it when a leg shows open again (see [FEED.md](FEED.md), "The apply"). The Clerical board reads it, so the packet moves from "Ready to close" to "Ready for data book" by itself. SET_VCLOSED on a VISUAL packet is refused as `wrong_status`: "The VISUAL feed marks this job closed when it sees every work order closed."
- A hand packet has no feed state. SET_VCLOSED is Clerical, in status complete, only when the packet is routed to the data book (`POST /api/packets/{id}/vclosed` `{closed}`). Confirming sets `vclosed_at` and `vclosed_by`. Taking it back clears both ("Not closed in VISUAL, move back"). A hand packet routed to Shipping files without it.
The packet read carries `vclosed` (`{at, by}`; `by` is null when the feed set it) and `vclose_check`, outside the seal. `vclose_check` is null for a hand packet. For a feed packet: `observed_at` and `closed_at` from the feed's VISUAL state, `open_wos` (the legs whose status is not `C` and not cancelled, as `wo_base/lot` text, never cast), and `agrees`. The precondition `closed_in_visual` reads `vclosed_at` and, on a VISUAL packet, the open legs.
### Filing
FILE is Clerical, from complete, held by an open change order, signed ("File in the data book"), with the precondition `vclosed_if_databook` ("Close the job in VISUAL first.").
FILE is Clerical, from complete, held by an open change order, signed ("File in the data book"), with three preconditions. `blocked.file` lists every one that fails, so the close panel says why.
When `vclose_check.open_wos` is not empty, the transition must carry `confirm_open_wos: true`. Without it the answer is 409 `open_work_orders` "VISUAL still shows 2 work orders open on this packet: L489052/1, L489052/2. File anyway?" with `work_orders` in the body. It is a warning, not a block, on both routes. The check runs after the policy and before the signature, so a cancelled confirm costs no PIN attempt. With the confirm, the seal keeps the work orders in `open_wos` and the log note says so. A hand packet has no feed state and no warning.
- `closed_in_visual` (Gitea issue 31, change C5). A VISUAL packet, on both routes: no work order open in the feed's state and `vclosed_at` set by the feed. While a work order is open: "VISUAL still shows work orders open on this packet: L489052/1, L489052/2. Filing waits until VISUAL closes them." With none open but the close not yet seen: "Filing waits until the VISUAL feed sees the job closed." A hand packet: "Close the job in VISUAL first." on the Data Book route until Clerical confirms; none on Shipping.
- `itp_complete` (Gitea issue 25, change C3): the unit's ITP is complete. An ITP that is open, signed or reopened blocks with "Complete the unit's ITP first. Q.C. signs it off on the ITP page." A unit with no ITP does not block.
- `shorts_approved` (Gitea issue 18, change C7): no open short and every receipt approved by the supervisor.
Filing is blocked, not warned. The phase 7 second confirmation (`confirm_open_wos` and the 409 `open_work_orders`) is gone; the server ignores `confirm_open_wos` if a client still sends it. New seals never record `open_wos`; older seals keep theirs.
In one transaction, filing sets status filed and `databook_at`, writes the seal, builds the data book, stores it as an asset on the seal, and writes the log and audit rows. A data book that cannot be built answers 500 `databook_failed` "The data book could not be built: <reason>. Nothing was filed." and nothing is written. A part that cannot be read (a missing file, an unreadable MTR) does not fail the filing: it becomes a warning row in the data book index.
@@ -667,15 +702,17 @@ Every read below needs a signed-in session and a packet that exists and is not r
`?download=1` serves the file as an attachment. `?sheet=n` limits `map-h`, `map-w` and `clean` to packet page n. Prints and exports write no `packet_log` row, because they change nothing on the packet. The History tab lists them from `GET .../prints`.
A print in the app goes straight to the browser's print dialog from the page: the screen fetches the stamped print PDF (`print/{kind}`, or `/api/itps/{id}/print`), renders it into a hidden print area and prints it (Gitea issue 27, change C4, built 2026-10-30). Viewing a PDF still opens it in a tab. The endpoints, the stamps and the print log did not change.
### Log texts
The `action` and `note` of `packet_log` for close, file and reopen.
| Write | action | note |
|---|---|---|
| set_vclosed, confirm | `Job closed in VISUAL` | the packet's work order bases, `L489052, L489053`, plus ` · VISUAL still shows open: L489052/1` when the feed shows open legs |
| set_vclosed, confirm (hand packet) | `Job closed in VISUAL` | the packet's work order bases, `L489052, L489053` |
| set_vclosed, taken back | `Moved back to ready to close` | |
| file | `Filed in the data book` | the note given with the step, plus `Filed with work orders open in VISUAL: L489052/1, L489052/2` when confirmed past open work orders, joined with ` · ` |
| file | `Filed in the data book` | the note given with the step |
| file, the data book | `Data book filed` | `N pages` |
| admin_reopen | `Reopened by IT` | the reason |
| set_databook_at | `Data book date set` | |
@@ -688,7 +725,7 @@ Nothing in the rule table changes in phase 8. Project Management, the find bar f
### Who reads the Project Management endpoints
Every endpoint under `/api/pm` needs a signed-in session. Any role may read them, not only PS-ProjectMgmt, as every role may open every packet (open question 39). Changing: SethD decided on 2026-10-28 (Gitea issue 33) that only PS-ProjectMgmt and Admin may (PLAN.md change C10). An unsigned request is 401. PM writes nothing: there is no POST, PUT, PATCH or DELETE under `/api/pm`, and the PM role passes no write action anywhere else. A packet opened from the PM screen shows "View only." The reports read PostgreSQL only. They never read VISUAL or M-Files.
Every endpoint under `/api/pm` is for PS-ProjectMgmt (role `pm`) and Admin only (Gitea issue 33, change C10, built 2026-10-30; it replaced the phase 8 default of any signed-in role, open question 39). Any other role is 403 `forbidden` "Project Management only." An unsigned request is 401. No other screen calls `/api/pm`: the find bar and the boards read `/api/packets` and the board endpoints. The PM entry in the screen switcher shows only for PM and Admin, and anyone else who types `/pm` sees "The Project Management screen is not for your role." PM writes nothing: there is no POST, PUT, PATCH or DELETE under `/api/pm`, and the PM role passes no write action anywhere else. A packet opened from the PM screen shows "View only." The reports read PostgreSQL only. They never read VISUAL or M-Files.
| Endpoint | What |
|---|---|
@@ -708,7 +745,7 @@ Every PM endpoint takes the scope params: `project_id`, `unit_id`, `dept`, `stat
- Projects in scope: every project not removed. A closed project only when a packet in scope belongs to it.
- Units in scope: the project's units. When any filter other than the project is set, only the units with a packet in scope.
- Health follows the old healthOf. `empty` with no packets. `done` when every packet is filed. `late` past the due date. `risk` when due within 7 days and under 75% done, or when any packet has an open change order or a warehouse hold. Else `ok`. A unit uses its own due date, else the project's. A project is as healthy as its worst unit.
- Over target means station timer hours over the work-order target hours. VISUAL ticket hours are shown beside the timer and never decide the flag (open question 38).
- Over target means VISUAL ticket hours over the work-order target hours (Gitea issue 1, change C1, built 2026-10-30; it replaced open question 38). A packet with no ticket hours, or no target, is not over target. The station timer is shown beside the ticket hours and never decides the flag. The group tables' variance and ratio are ticket based ("Ticket variance h", "Ticket h / target", "Over target (tickets)"), the over-target table ends with "Over by (tickets)", and the KPI reads "Packets over target hours (tickets)". The CSV and the report PDF follow.
### Search
@@ -727,7 +764,7 @@ A child match never repeats a packet. IDs are matched as text, never cast.
### Filters
The server applies the filters on every list and board (open question 41). A board never fetches more than it shows. A filter only narrows what the person could already read. It never widens access, and each board keeps its own scope checks (a supervisor's departments, the stall's station).
The server applies the filters on every list and board (open question 41). A board never fetches more than it shows. A filter only narrows what the person could already read. It never widens access, and each board keeps its own scope checks (a supervisor's departments, the stall's own assignments).
`status` takes one of the words below, or a comma list of raw statuses as before (`status=ready,review`).
@@ -767,7 +804,7 @@ Mine is decided on the server from the person's AD roles and the screen they nam
| clerical | status draft, or status complete routed to the data book |
| warehouse | status warehouse, or an open short |
| supervisor | the departments where the person is lead or supervisor |
| stall | Changing to the packets assigned to the person, at their assigned stall (Gitea issue 36, PLAN.md change C11). Today: the person's shop departments and the picked station (`mine_station_id`). No station picked: nothing. |
| stall | the packets assigned to the person (`welder_id` is them), in status assigned, in progress or review, across stations. No station is needed; `mine_station_id` is accepted and ignored (Gitea issue 36, change C11, built 2026-10-30). |
| qc | status qc |
| pm, or admin with no other role | everything. The chip is hidden on the PM and Stall screens. |
@@ -830,11 +867,11 @@ The packet page shows who else has the packet open: `Also in this packet now: <N
| What the old build allowed | What the rebuild does | Why | Can be vetoed |
|---|---|---|---|
| The find bar filtered in the browser, over every packet the browser had loaded. | The server applies the search and every filter on each list and board. A filter only narrows what the person may read. | A board never fetches more than it shows. | No |
| "Mine" was worked out in the browser from the local session's role. | Mine is decided on the server from the person's AD roles and the screen they name. The stall's Mine needs the picked station. | The roles come from AD, not from what the browser keeps. | Yes |
| "Mine" was worked out in the browser from the local session's role. | Mine is decided on the server from the person's AD roles and the screen they name. The stall's Mine is the packets assigned to the person, across stations (Gitea issue 36, change C11). | The roles come from AD, not from what the browser keeps. | Yes |
| PM exports were built and saved in the browser, and the browser wrote the `report exported` log line. | Every export is built on the server and logged in `print_log` and the audit as `report exported`, with the scope, the period and, for the PDF, the page count. | The browser alone could skip the log. | Yes |
| The PM reports were on the PM home screen only. | Any signed-in role may read them, as any role may open any packet. PM still writes nothing. | The figures come from the same packets every role can already open. | Yes |
| The PM reports were on the PM home screen only. | Only PS-ProjectMgmt and Admin may read them, on the server (Gitea issue 33, change C10). PM still writes nothing. | SethD decided the reports are for Project Management. The phase 8 default let every role read them. | No (decided by SethD) |
| Each browser published its own presence, with the name, employee ID, role and stall, refreshed every 20 seconds. | The server builds presence from the session: display name and role, no employee ID. A locked or revoked session drops out at once. | Presence shows only what it needs, and a locked screen is not someone in the packet. | Yes |
| Over target was worked out in the browser from the hours it had. | Over target is timer hours over the work-order target, on the server. Ticket hours are shown beside it and never decide it. | The station timer is the clock of record (open question 1). | Yes |
| Over target was worked out in the browser from the hours it had. | Over target is VISUAL ticket hours over the work-order target, on the server. The station timer is shown beside them and never decides it (Gitea issue 1, change C1). | SethD decided the ticket hours are the measure. The timer stays as a clock. | No (decided by SethD) |
## Phase 9: Admin, notifications, reconcile
@@ -842,7 +879,7 @@ Nothing in the packet rule table changes in phase 9. The new endpoints are Admin
### Who may use the Admin endpoints
Every endpoint under `/api/admin` needs an Admin (PS-Admins) in a password session. An Admin in a PIN session is 403 `password_session_required`. Every other role is 403. An unsigned request is 401. Every write also passes the Origin check and the body limit. Tests try every endpoint as every role.
Every endpoint under `/api/admin` needs an Admin (PS-Admins) in a password session, except the stamp check (see "The stamp check"). An Admin in a PIN session is 403 `password_session_required`. Every other role is 403. An unsigned request is 401. Every write also passes the Origin check and the body limit. Tests try every endpoint as every role.
| Endpoint | What | Audit |
|---|---|---|
@@ -852,14 +889,14 @@ Every endpoint under `/api/admin` needs an Admin (PS-Admins) in a password sessi
| `GET /users`, `GET /users/{id}` | Gain the welder stamp, `profile_active`, station limits and the directory flags | none |
| `PATCH /users/{id}/profile` | Welder stamp, `profile_active`, station limits per department | `admin.profile_updated` |
| `POST /users/import` | The profile CSV, as a preview (`apply: false`) or applied | `admin.profile_updated` per changed person, `admin.profile_import` |
| `GET /checklists`, `PUT /checklists` | The weld and plain station checklists | `admin.checklists_updated` |
| `GET /checklists`, `PUT /checklists` | The default weld and plain lists, used by a department without its own list (see "Checklists"). The Admin screen no longer calls these; its Checklists tab edits department lists. | `admin.checklists_updated` |
| `GET /security/settings`, `PUT /security/settings` | The session, lock and sign-in limit settings | `admin.security_settings_updated` |
| `GET /security/alerts?hours=` | Security alerts for 1 to 168 hours | none |
| `GET /security/integrity` | The newest finished integrity run and its findings | none |
| `GET /access-log` | Sign-in attempts and identity audit rows for everyone | none |
| `GET /access-log.csv` | The same, as CSV, at most 50,000 rows | `admin.access_log_exported`, a `print_log` row of kind `access-log-csv` |
| `GET /presence` | Live sessions, locked or not, with the screen and packet | none |
| `POST /stamp-verify`, `POST /stamp-verify/pdf` | Checks a stamp line, a 16-character code or a PDF (25 MB) against the print log | `admin.stamp_verified` |
| `POST /stamp-verify`, `POST /stamp-verify/pdf` | Checks a stamp line, a 16-character code or a PDF (25 MB) against the print log. Kept as aliases of `/api/stamp-verify`, open to Admin, Q.C. and Clerical (see "The stamp check") | `admin.stamp_verified` |
| `GET /directory-flags` | Active users with no employee ID, and employee IDs on more than one account | none |
| `POST /ad/refresh` | Asks the worker for an `ad-reconcile` run; 202 (open question 48) | `ad.refresh_requested` |
| `GET /ad/reconcile` | The newest finished reconcile run and its report | none |
@@ -891,9 +928,21 @@ The checklists, the security settings and each notification rule carry a version
### Checklists
Changing: SethD decided (Gitea issue 23, PLAN.md change C8) that checklists become per department, edited by that department's supervisor or Admin, and a change applies to packets not yet started only.
Each shop department has its own checklist (Gitea issue 23, change C8, built 2026-10-30). It replaced the two Admin lists of phase 9 and open questions 26 and 52.
Both lists need at least one item and at most 40. A key is lower case letters, digits and underscores, starting with a letter, at most 30 characters, unique in its list. Group and text are not empty, at most 200 characters. The only placeholders are `{drawing}`, `{rev}` and `{wps}`. Answers on open stages are kept by key. A removed item drops out of new checklists only. An item added while a stage is open must also be ticked on that stage before submit, since the completeness check reads the live list.
| Endpoint | Who | What |
|---|---|---|
| `GET /api/checklists` | any signed-in session | every active shop department in sort order: `{dept_code, dept_name, weld, custom, version, editable}` |
| `GET /api/checklists/{dept}` | any signed-in session | the same plus `items` (placeholders not filled) and `defaults` (the default list for its kind). An unknown or support department is 404 `not_found` "No such shop department." |
| `PUT /api/checklists/{dept}` `{items, base}` | that department's Supervisor, or Admin, in a password session | the department's list; answers the GET shape. Audit `checklists.dept_updated` (subject `settings` / `checklist:<code>`, detail `{dept, before, after}`) |
- Who edits: `editable` is true for Admin and for a Supervisor of that department. A lead, a worker, Q.C., Clerical, Warehouse and PM do not edit department checklists. A save by anyone else is 403 `forbidden` "Only this department's supervisor or an Admin can change its checklist." An editor in a PIN session is 403 `password_session_required`.
- Compare and set: the save sends the version it started from as `base`, or as `If-Match`. With neither the answer is 428 `if_match_required` "Send the version you started from." A stale version is 409 `version_conflict` "Someone else changed this checklist. Reload and try again." with `current`, the GET shape.
- `items: null` sets the department back to the default for its kind.
- A list needs at least one item and at most 40. A key is lower case letters, digits and underscores, starting with a letter, at most 30 characters, unique in its list. Group and text are not empty, at most 200 characters. The only placeholders are `{drawing}`, `{rev}` and `{wps}`. Anything else is 422 `bad_checklist` with `errors`, one line per problem.
- Default lists: a department without its own list uses the default for its kind, the weld or plain list of the `checklist_templates` row, else the code's defaults. `/api/admin/checklists` still edits that row.
- Frozen at START: START copies the department's live list onto the stage (`packet_stages.checklist_template`), once. A change reaches only stages not yet started. Every reader uses the stage's frozen list when it is set, else the department's live list: the checklist items in the packet read, `checklist_complete`, the key check on a tick (422 for a key not in the list) and the data book's weld record. Answers are kept by key.
- Ticking items (EDIT_CHECKLIST) stays the holder's. The Q.C. check items are not per department and stay Admin only.
### Security settings
@@ -901,12 +950,14 @@ Stored as the `security` settings row over the environment defaults. A value out
### Access log, presence and the stamp check
Changing: the stamp check opens to Q.C. and Clerical as well as Admin (Gitea issue 32, PLAN.md change C9).
- The access log shows every person's sign-in attempts (successful and failed) and their `auth.*`, `pin.*`, `device.*` and `sig.*` audit rows, as the Account access log shows a person their own. The address filter takes an address or a range. Request bodies and secrets are never in it.
- Who is in shows live sessions (not revoked, not expired) seen within the idle minutes that apply to each. The screen and packet come from the event stream, matched by person.
- The stamp check never trusts the text. It finds the 16-character signature, looks it up in `print_log`, recomputes it with the seal key from the logged row and compares the text with the logged stamp line. It answers genuine, does not verify (the signature or the text differs from the log), or not found.
### The stamp check
Built 2026-10-30 (Gitea issue 32, change C9). `POST /api/stamp-verify` and `POST /api/stamp-verify/pdf` take the same bodies and give the same answers as the Admin paths, which stay as aliases with the same rule. Who: Admin, every Q.C. role (`qc`, `qc_supervisor`) and every Clerical role (`clerical`, `clerical_supervisor`). Anyone else is 403 `forbidden` "The stamp check is for Admin, Q.C. and Clerical." A PIN session is 403 `password_session_required`. The audit row stays `admin.stamp_verified`. The screen is `/stamp-check`, linked in the top bar for those roles; Admin's Stamp check tab uses the same endpoints.
### Notifications
- An Admin edits every rule. Recipients are any AD group under the base DN, a user, a mailbox, or (for the department events, `co_waiting` and `qc_reject`) the Supervisor group of the event's department. At most 50 recipients and 200 exclusions per rule.
@@ -1007,7 +1058,7 @@ SethD answered on 2026-10-26: every row is kept as shown (Gitea issue 6), and he
| Any worker in the department within their station limits could start or submit a packet (`worksOn` ignored who was assigned). | START and SUBMIT need the assigned welder for a worker. Leads and supervisors still can. | The README says only the assigned worker. | Yes |
| A status change could carry any other field in the same save. | A transition carries only its own fields (the step's inputs). Other changes go through their own edit actions and rules. | One rule per write keeps the check honest. | No |
| Clerical "lead" actions (approve an M-Files rev mismatch, move back from VISUAL review) were open to any Clerical member with a lead flag. | REV_APPROVE and moving back from VISUAL review need Clerical Supervisor (PS-Clerical-Supervisor). | There is no Clerical lead AD group. | Yes |
| A packet routed to Shipping had no path to filed. | Complete packets routed to Shipping get the same Clerical close and file path. Only the Data Book route needs the VISUAL close before filing. | Shipping packets were stuck at complete. | Yes |
| A packet routed to Shipping had no path to filed. | Complete packets routed to Shipping get the same Clerical close and file path. A VISUAL packet files on either route only once the feed sees the job closed in VISUAL (Gitea issue 31, change C5). A hand packet needs the Clerical close confirm on the Data Book route only. | Shipping packets were stuck at complete. | Yes |
| Anyone could set the data book date, even on a filed packet. | SET_DATABOOK_AT is Clerical or Q.C. only (open question 17). It stays allowed on a filed packet. | The data book date is a Clerical and Q.C. record. | Yes |
| Change-order verifications were stored in the packet document, with no rule against changing them later. | Verifications need a signature, are insert-only and cannot be withdrawn (open question 18). | A signed verification is a record. | Yes |
| The old policy did not look at the BOM when the warehouse released a packet. | WH_RELEASE needs every BOM line not marked n/a to be pulled or to have an open short. | A packet should reach the shop with its material, or with the shorts on record. | Yes |
@@ -1027,9 +1078,13 @@ SethD answered on 2026-10-26: every row is kept as shown (Gitea issue 6), and he
| The station clock ran in the browser, and the elapsed time was saved from the browser. | The server keeps the clock with the database time. No request sets the elapsed time. | The clock is a record. A browser clock can be wrong or changed. | No |
| Packet document kinds and Q.C. folder document types were whatever the screen sent. | Both are closed sets checked on the server (and by a CHECK in the database). | The data book sorts and labels by kind and type. | Yes |
| A packet document could be taken out of the data book with no reason. | A take-out needs a reason of at least 3 characters. The row is kept with who, when and why. | A document leaving the record is a decision on record. | Yes |
| A Q.C. reject took any non-empty text. | A reject needs a reason of at least 8 characters, or a well-formed NCR number, or both (open question 27). Changing: SethD decided on 2026-10-26 (Gitea issue 24) that a reason is always needed and acceptance needs an NCR number (PLAN.md change C2). | A short note like "no" says nothing to the supervisor. An NCR number points to the full record. | Yes |
| A Q.C. reject took any non-empty text. | A reject always needs a reason of at least 8 characters. An NCR number may go with it but no longer stands in for it. A packet rejected without an NCR number cannot be accepted until one is given (Gitea issue 24, change C2, built 2026-10-30; it replaced open question 27). | A short note like "no" says nothing to the supervisor. An NCR number points to the full record. | No (decided by SethD) |
| An ITP line rejected without an NCR could not be approved only because the screen disabled the button. | The server refuses the approval until the NCR number is entered (409 `ncr_required`). | The screen alone could be skipped. | Yes |
| ITP block signatures and the sign-off were PIN checks in the browser. | Both are signatures verified on the server and stored with a content hash. | A signature is a record. | No |
| The ITP sign-off was offered when the screen saw every block signed. | The sign-off is a server rule: every block signed (409 `blocks_unsigned`), and a complete ITP refuses every write but the reopen. | The screen alone could be skipped. | Yes |
| The screen limited removing an ITP line's initials to the same inspector or an admin, and refused it while the block was signed. Nothing else checked. | The server enforces both (open question 18). | The initials are that inspector's record. The screen alone could be skipped. | Yes |
| Any Q.C. user could remove a block signature. | Only the signer or an admin, while the ITP is open (open question 30). | A signature is that person's record. | Yes |
| Clerical marked a job closed in VISUAL by hand, and nothing checked VISUAL. | On a VISUAL packet the feed sets the close when it sees every work order closed, and FILE is blocked while VISUAL shows one open, on both routes. Clerical confirms only hand packets (Gitea issue 31, change C5). | The data book should not be filed for a job still open in VISUAL. | No (decided by SethD) |
| A packet could be filed while its unit's ITP was still open. | FILE needs the unit's ITP complete when the unit has one (Gitea issue 25, change C3). | The data book should not be built from an incomplete ITP. | No (decided by SethD) |
| A received short needed no further check. | The department supervisor approves each receipt with a signature. Q.C. accept to complete and filing wait for every approval (Gitea issue 18, change C7). | The supervisor checks the part and its heat number against what the warehouse issued. | No (decided by SethD) |
| One checklist for weld departments and one for the rest, the same in every department of a kind. | Each department has its own checklist, edited by its supervisor or Admin. A stage keeps the list it started with (Gitea issue 23, change C8). | Each department knows its own work. A list changed during a stage would ask for items the work did not plan for. | No (decided by SethD) |
+18 -7
View File
@@ -1,6 +1,6 @@
# Packet Station schema
Status: phases 1 to 4 built (identity, organisation, projects, packets, change orders, documents, jobs, VISUAL and drawing caches, the warehouse columns in use, the `notification_events` placeholder). Phase 5 adds the shop columns, the checklist templates and the clock rules (migration 0006). Phase 6 adds the Q.C. columns, the Q.C. check items, the Q.C. folder metadata, the ITP header, line and block details and the ITP log (migration 0007). Phase 7 adds the print log and the data book on the seal (migration 0008). Phase 8 adds no table and no migration: the PM report exports use new `print_log` kinds and presence uses two new session events. Phase 9 adds the notification rules and delivery, the integrity findings and the Admin profile switch (migration 0010). The DDL is in `server/packetstation/migrations/`; this file explains it. See [PLAN.md](PLAN.md), "Schema".
Status: phases 1 to 4 built (identity, organisation, projects, packets, change orders, documents, jobs, VISUAL and drawing caches, the warehouse columns in use, the `notification_events` placeholder). Phase 5 adds the shop columns, the checklist templates and the clock rules (migration 0006). Phase 6 adds the Q.C. columns, the Q.C. check items, the Q.C. folder metadata, the ITP header, line and block details and the ITP log (migration 0007). Phase 7 adds the print log and the data book on the seal (migration 0008). Phase 8 adds no table and no migration: the PM report exports use new `print_log` kinds and presence uses two new session events. Phase 9 adds the notification rules and delivery, the integrity findings and the Admin profile switch (migration 0010). SethD's changes (built 2026-10-30) add the short approvals and the checklist frozen on each stage (migration 0011), and the per-department checklist settings rows. The DDL is in `server/packetstation/migrations/`; this file explains it. See [PLAN.md](PLAN.md), "Schema".
## Conventions
@@ -65,7 +65,7 @@ Smaller closed sets are text with a CHECK: `projects.source` and every `*_src` (
`packets` is the header. The columns that matter:
- Placement: `unit_id`, `dept_code`, `op`, `title`, `customer`, `drawing`, `rev`, `wps`.
- Workflow: `status` (packet_status), `status_at`, `station_id`, `welder_id` and `welder_stamp` (a snapshot taken at assign), `for_sup_id`, `wh_skipped` (true when Clerical sent the draft straight to a department or to Q.C.), `wh_sent_at`, `routed_to` (route_kind, set at Q.C. accept), `return_note` (the reason shown after a send-back), `vclosed_at` and `vclosed_by`, `databook_at`.
- Workflow: `status` (packet_status), `status_at`, `station_id`, `welder_id` and `welder_stamp` (a snapshot taken at assign), `for_sup_id`, `wh_skipped` (true when Clerical sent the draft straight to a department or to Q.C.), `wh_sent_at`, `routed_to` (route_kind, set at Q.C. accept), `return_note` (the reason shown after a send-back), `vclosed_at` and `vclosed_by` (closed in VISUAL: on a VISUAL packet the feed writes it with `vclosed_by` null, on a hand packet Clerical; see "Seth's changes"), `databook_at`.
- Preconditions: `heat_map` is nullable on purpose. Null means not answered yet, and the warehouse step needs an answer.
- Source: `source_kind` (visual or hand), `source` jsonb, `visual_key` (the feed's match key), `visual_rel`, `visual_want`, `drawing_state`, `flags` text[], `visual_fp_applied` (the fingerprint the packet matches), `visual_closed_at`, `visual` jsonb (system-written sub-keys).
- Counters: `next_weld_no`, `next_heat_no`, `next_page_no` and `next_co_no`, all starting at 1. See "Versioning".
@@ -82,6 +82,7 @@ The trigger `ps_packet_touch` sets `updated_at` on every UPDATE. It does not tou
- `bom_lines`: `item`, `qty`, `part_id` (text), `description`, `dwg_ref`, `cl_item`, `length`, `material`, `heat`, `na`, `issued`, `loc`, `pulled`, `pulled_at`, `pulled_by`, `mtr_asset_id`, `src`, `vkey`, `whs`, `comm`, `cons`, `sub`, `grp`, `sort`, `row_version`. Unique on (`packet_id`, `item`).
- `bom_shorts`: `bom_line_id` (cascades with the line), `qty`, `qty_open`, `reason`, `ref`, `eta`, `note`, `opened_at`, `opened_by`, `closed_at`, `row_version`. At most one open short per line (unique on `bom_line_id` where `closed_at` is null).
- `short_receipts`: append-only. `short_id`, `packet_id`, `qty`, `heat`, `note`, `at`, `by`.
- `short_approvals` (migration 0011): append-only. The supervisor's approval of one receipt. See "Seth's changes".
What the warehouse columns mean in practice (phase 4):
@@ -95,7 +96,7 @@ What the warehouse columns mean in practice (phase 4):
- `bom_shorts`: one open short per line at most. Opening a short sets the line's `issued` to `qty` minus the short quantity and ticks it pulled. `qty` is what was short in total, `qty_open` what is still missing. `reason` is one of the closed short reasons, `ref` the PO or requisition, `eta` the expected date. Editing an open short keeps what was already received in `qty`. "Not short after all" closes it (`closed_at`) and sets `issued` to the line's `qty`. While a short is open, the line's heat, issued and location change only through the short.
- `short_receipts`: one row per delivery, never changed. Each receipt lowers `qty_open`, adds its pieces to the line's `issued` and its heat to the line's `heat`. The receipt that brings `qty_open` to 0 closes the short.
- `packet_wh`: `reason` is one of the closed hold reasons when the stage is `hold`, and `note` is up to 200 characters.
- `packet_stages`: one row per visit to a shop department. `dept_code`, `station_id`, `welder_id`, `welder_stamp`, `checklist` jsonb (key to `{done, at, by, stamp}`, written per key with `jsonb_set`), `filler`, `filler_lot`, `preheat`, `notes`, the clock (`clock_elapsed_ms`, `clock_running_since`, `clock_started_at`; only the server writes them, see "The clock"), `qc_snapshot`, `opened_at`, `closed_at`, `row_version`. At most one open stage per packet.
- `packet_stages`: one row per visit to a shop department. `dept_code`, `station_id`, `welder_id`, `welder_stamp`, `checklist` jsonb (key to `{done, at, by, stamp}`, written per key with `jsonb_set`), `filler`, `filler_lot`, `preheat`, `notes`, the clock (`clock_elapsed_ms`, `clock_running_since`, `clock_started_at`; only the server writes them, see "The clock"), `qc_snapshot`, `opened_at`, `closed_at`, `row_version`. At most one open stage per packet. Migration 0011 adds `checklist_template`, the list frozen at START (see "Seth's changes").
- `welds`: `weld_no` (from `next_weld_no`), `page`, `fx` and `fy` (fractions of the page), `bfx` and `bfy` (0006), `wo_base`, `piece`, `items` int[], `type`, `size`, `process`, `stamp`, `welder_id`, `dept_code`, `stage_id`, `carried_from`, `at`, `by`, `row_version`, soft delete. Unique on (`packet_id`, `weld_no`) among live rows.
- `heat_tags`: `tag_no` (from `next_heat_no`), `page`, `item`, `fx`, `fy`, `bfx` and `bfy` (0006), `carried_from`, `at`, `by`, `row_version`, soft delete. Unique on (`packet_id`, `tag_no`) among live rows.
- `heat_table_placements`: `page`, `x`, `y`, `s`, `carried_from` (0006), `by`, `at`. Unique on (`packet_id`, `page`), written as an upsert.
@@ -154,9 +155,9 @@ The `settings` row `checklist_templates` holds the shop checklists. Migration 00
| plain | Before starting | `dwg`, `prev`, `mat` |
| plain | When done | `work`, `insp`, `qty` |
When the row is missing or malformed, the defaults in `domain/checklists.py` apply and a warning is logged. The read model and the checklist write both use the loaded templates. No screen edits the row in phase 5. Phase 9 (Admin) adds that.
When the row is missing or malformed, the defaults in `domain/checklists.py` apply and a warning is logged. The read model and the checklist write both use the loaded templates. No screen edits the row in phase 5. Phase 9 (Admin) adds that. Since change C8 (built 2026-10-30) this row holds the default lists by kind, and each department may have its own list (see "Seth's changes").
`packet_stages.checklist` maps each key to `{done, at, by, stamp}`. `by` is the user id. `stamp` is the user's `welder_stamp`, else the packet's. A key not in the department's list is 422.
`packet_stages.checklist` maps each key to `{done, at, by, stamp}`. `by` is the user id. `stamp` is the user's `welder_stamp`, else the packet's. A key not in the stage's list (the frozen `checklist_template`, else the department's live list) is 422.
### Carried marks
@@ -238,7 +239,7 @@ The texts may hold `{wps}` (else `the WPS`), `{stamp}` (`[75] ` with a trailing
### Column additions
- `packet_seals`: `databook_asset_id` (uuid, references `assets`), the data book PDF built and stored when the packet was filed. `databook_pages` (int), its page count. `open_wos` (text[]), the work orders the feed showed open in VISUAL when Clerical filed anyway, as `wo_base/lot` text (`L489052/1`), or null.
- `packet_seals`: `databook_asset_id` (uuid, references `assets`), the data book PDF built and stored when the packet was filed. `databook_pages` (int), its page count. `open_wos` (text[]), the work orders the feed showed open in VISUAL when Clerical filed anyway, as `wo_base/lot` text (`L489052/1`), or null. Since change C5 (built 2026-10-30) filing is blocked while VISUAL shows a work order open, so new seals leave it null; older seals keep theirs.
### The print log
@@ -339,7 +340,7 @@ A unit folder shows its own current rows and inherits the project's current rows
These tables have the `ps_append_only` row and TRUNCATE triggers, and `ps_app` holds only SELECT and INSERT on them:
`audit_events`, `signin_attempts`, `signatures`, `project_renumbers`, `short_receipts`, `packet_transitions`, `qc_history`, `packet_log`, `itp_rejects`, `itp_log` (0007), `print_log` (0008), `notifications_sent` and `integrity_findings` (0010), `co_verifications`.
`audit_events`, `signin_attempts`, `signatures`, `project_renumbers`, `short_receipts`, `packet_transitions`, `qc_history`, `packet_log`, `itp_rejects`, `itp_log` (0007), `print_log` (0008), `notifications_sent` and `integrity_findings` (0010), `short_approvals` (0011), `co_verifications`.
The app role is refused by privilege first. The migrate role owns the tables, so the trigger refuses it. The views `v_units` and `v_open_cos` are SELECT only for `ps_app`.
@@ -378,6 +379,16 @@ Phase 9, migration 0010, builds the delivery on this table:
- `integrity_findings`: append-only. One row per thing the integrity job found wrong: `run_id` (no foreign key, since housekeeping prunes `job_runs`), `at`, `kind` (seal, asset_hash, asset_missing, blob_orphan, audit_chain), `subject` (the packet, asset, file path or audit id), `detail` jsonb.
- Settings rows used by phase 9: `security` (the Admin's overrides of the session, lockout and rate-limit settings; missing keys fall back to the environment), `checklist_templates` (now edited on the Admin Checklists tab), `notify.last_build` (the build the worker last announced).
## Seth's changes (built 2026-10-30, migration 0011)
Migration 0011 serves changes C7 and C8 in [PLAN.md](PLAN.md). The others need no schema.
- `short_approvals` (new, append-only; Gitea issue 18, change C7): the department supervisor's approval of one short receipt. `receipt_id` (unique, references `short_receipts`), `short_id` (references `bom_shorts`), `packet_id`, `dept_code` (the department the packet was in at the approval), `note`, `signature_id`, `at`, `by`. Indexed on `packet_id`. A second approval of a receipt breaks the unique key; the API answers 409 `already_approved`. The packet read and the board cards count `shorts_unapproved`: open shorts plus receipts with no row here.
- `packet_stages.checklist_template` (jsonb, new; Gitea issue 23, change C8): the checklist the stage froze at START, as a list of `{group, key, text}` with the placeholders not filled. NULL means the stage has not started and reads its department's live list. START sets it once, only when NULL. Every reader uses it when set. 0011 backfills it on the open stage of every packet in progress, in review or at Q.C. with the default list for the stage's kind from the `checklist_templates` row, so a department edit does not reach work already started. When that row is missing or its list is not an array, the stage stays NULL and reads the department's live list until it closes.
- Settings rows `checklist:<dept code>` (no migration; for example `checklist:03`): one department's own checklist, `{"items": [{group, key, text}, ...]}`. `{"items": null}` sets the department back to the default for its kind and keeps the row's `version` for compare and set. A row that does not parse is logged and the default applies. A department with no row uses the default for its kind: the `weld` or `plain` list of `checklist_templates`, else the code's defaults. The department's Supervisor or an Admin saves it through `PUT /api/checklists/{dept}` (see [POLICY.md](POLICY.md), "Checklists").
- `packets.vclosed_at` and `vclosed_by` (no migration; Gitea issue 31, change C5): on a VISUAL packet the feed is now the one writer. It sets `vclosed_at` (with `vclosed_by` null) when every leg is closed or cancelled and clears both when a leg shows open again. On a hand packet Clerical still sets them. See [FEED.md](FEED.md), "The apply".
- `qc_inspections.ncr` (no migration; Gitea issue 24, change C2) is now also written by the accept that carries the NCR number of an earlier reject.
## VISUAL and drawing caches (phase 3)
Migration 0004. Every ID part is text (`wo_base`, `lot`, `split`, `sub`, part IDs, order IDs), with `''` in place of null in a key. `seq` and `piece` are int. Closed sets are text with a CHECK. The worker fills these tables. Screens read them, never VISUAL. [FEED.md](FEED.md) says how each is written.
+21 -9
View File
@@ -2,7 +2,7 @@
This document is rewritten for the server build. The old browser build's security document is in the old repo (`C:\Users\spencerm\Coding\PacketStation\docs\SECURITY.md`). The design comes from [BRIEF.md](BRIEF.md) and [PLAN.md](PLAN.md).
Status: phase 9. The identity controls (sessions, PIN, rate limits, audit, Origin check, derived group map) and the policy controls (server policy on every write, workflow steps as compare-and-set, server-side validation and limits, IT edit reasons, the filed lock, the upload pipeline for drawings and documents) are built and tested. Phase 3 adds the worker controls (read-only VISUAL and M-Files reads, stack guard, advisory locks, redacted errors, audited system writes). No requirement ID closes in phase 3. Phase 4 completes the upload pipeline (a kind per upload with its own types and limit, XLSX and CSV by content, the optional ClamAV scan, the assets volume quota alert) and adds the warehouse rules (the heat gate on release, closed hold and short reasons, receive by custody). Phase 4 closes D5. Phase 5 adds the shop rules (the server as the clock of record, server-assigned mark numbers under the packet row lock, insert-only change-order verifications with a signature, VOID sheets never edited). Phase 6 adds the Q.C. rules (server gates on accept, block sign and sign-off, NCR numbers checked by pattern, Q.C. documents never deleted, the append-only ITP log, server-verified signatures on every Q.C. and ITP sign-off). No requirement ID closes in phase 6. Phase 7 adds the close and file controls (the HMAC seal with the data book filed on it, the controlled-copy and print stamps signed with the seal key, `print_log` and audit rows for every print and export, the CSV formula guard, PDF metadata, escaping in the PDFs). Phase 7 closes M2, S6, S7 and S8. Phase 8 adds the Project Management reads and exports, the server-side find bar filters and the presence rules (display name and role only, locked and revoked sessions dropped). No requirement ID closes in phase 8. Phase 9 adds the nightly AD reconcile and the 15-minute role refresh, the integrity job with its alert, email notifications over verified STARTTLS with security events nobody can mute, the security alerts and the Security tab settings, the Admin tabs (every change audited), nightly backups with a rehearsed restore, and dependency scanning in CI. Phase 9 closes the app part of I6, M3, O1, O2 and S3. Every other control names the phase that builds it.
Status: phase 9. The identity controls (sessions, PIN, rate limits, audit, Origin check, derived group map) and the policy controls (server policy on every write, workflow steps as compare-and-set, server-side validation and limits, IT edit reasons, the filed lock, the upload pipeline for drawings and documents) are built and tested. Phase 3 adds the worker controls (read-only VISUAL and M-Files reads, stack guard, advisory locks, redacted errors, audited system writes). No requirement ID closes in phase 3. Phase 4 completes the upload pipeline (a kind per upload with its own types and limit, XLSX and CSV by content, the optional ClamAV scan, the assets volume quota alert) and adds the warehouse rules (the heat gate on release, closed hold and short reasons, receive by custody). Phase 4 closes D5. Phase 5 adds the shop rules (the server as the clock of record, server-assigned mark numbers under the packet row lock, insert-only change-order verifications with a signature, VOID sheets never edited). Phase 6 adds the Q.C. rules (server gates on accept, block sign and sign-off, NCR numbers checked by pattern, Q.C. documents never deleted, the append-only ITP log, server-verified signatures on every Q.C. and ITP sign-off). No requirement ID closes in phase 6. Phase 7 adds the close and file controls (the HMAC seal with the data book filed on it, the controlled-copy and print stamps signed with the seal key, `print_log` and audit rows for every print and export, the CSV formula guard, PDF metadata, escaping in the PDFs). Phase 7 closes M2, S6, S7 and S8. Phase 8 adds the Project Management reads and exports, the server-side find bar filters and the presence rules (display name and role only, locked and revoked sessions dropped). No requirement ID closes in phase 8. Phase 9 adds the nightly AD reconcile and the 15-minute role refresh, the integrity job with its alert, email notifications over verified STARTTLS with security events nobody can mute, the security alerts and the Security tab settings, the Admin tabs (every change audited), nightly backups with a rehearsed restore, and dependency scanning in CI. Phase 9 closes the app part of I6, M3, O1, O2 and S3. On 2026-10-30 SethD's changes C1 to C11 were built (see "SethD's changes (built 2026-10-30)"); they close D7. Every other control names the phase that builds it.
Contents:
@@ -74,9 +74,9 @@ The M: drive build had 20 layers of protection, most of them enforced in the bro
- One policy in Python on the server (`server/packetstation/policy/`), pure functions with no I/O. Every write goes: session, policy, transition, validation, version check, packet log row, audit row, NOTIFY, in one transaction with the packet row locked. Nothing in the browser decides.
- Every packet read returns `can: [...]`, `blocked: {...}` and `holder`, so the browser enables controls from the server's answer. The server re-checks on the write.
- A workflow step taken twice is refused with 409 and the current state (compare-and-set on the expected status and department). Child rows carry a row version checked with If-Match. Weld, heat tag, page and change order numbers are assigned by the server under the lock.
- Preconditions (heat map answered, drawing attached, BOM pulled or short, heats recorded on pulled lines, checklist complete, welds recorded, Q.C. items complete, closed in VISUAL before filing) run on the server and are never bypassed, Admin included.
- Preconditions (heat map answered, drawing attached, BOM pulled or short, heats recorded on pulled lines, checklist complete, welds recorded, Q.C. items complete, an NCR number on the accept after a reject without one, every short received and approved before the Q.C. accept to complete and before filing, the unit's ITP complete before filing, closed in VISUAL before filing) run on the server and are never bypassed, Admin included.
- Admin edits outside custody need an IT reason of at least 8 characters, recorded in the packet log and the audit row (`it_reason`). An Admin cannot write a filed packet except through the reopen action, which needs a reason and a signature and breaks the seal.
- Signatures (PIN or domain password, verified in the same transaction; the PIN is enough everywhere, Q.C. accept and filing included, confirmed by SethD on 2026-10-26, Gitea issue 2) on submit, send to Q.C., Q.C. accept and reject, route, file, reopen, change order issue, approve, reject and verify, the revision approval and hand drawing uploads.
- Signatures (PIN or domain password, verified in the same transaction; the PIN is enough everywhere, Q.C. accept and filing included, confirmed by SethD on 2026-10-26, Gitea issue 2) on submit, send to Q.C., Q.C. accept and reject, route, file, reopen, change order issue, approve, reject and verify, the revision approval, hand drawing uploads and the supervisor's approval of a received short.
- Input: every string is cleaned (control and invisible characters removed, NFC, trimmed) and cut to its field limit; heat numbers, stamps, project numbers and lots are checked against the old patterns; JSON bodies over 200 KB are refused.
- Uploads: typed by content (PDF, PNG, JPEG), 25 MB limit read in chunks, sha256, stored by id under the assets volume, served only with a session and with `Content-Disposition` and `X-Content-Type-Options: nosniff`. Phase 4 completes it (see "Uploads and storage").
- The filed lock: a filed packet accepts only the data book date and the Admin reopen. Filing writes an HMAC-SHA256 seal with the server seal key; a reopen keeps the old seal with who broke it and why.
@@ -157,7 +157,7 @@ The M: drive build had 20 layers of protection, most of them enforced in the bro
- Every PDF and the CSV are built on the server. The browser only opens the URL. Reads need a signed-in session; every role that can open the packet may read them, PM included.
- Controlled-copy stamp (S7): every exported page carries `Controlled copy - exported by Name (emp) <UTC time>`, the seal's first 12 hex or `not yet filed`, and ` - stamp <sig>`. The filed data book is the stored copy, served as it was built.
- Printed-copy stamp (S8): every printed page carries `PRINTED COPY - <what> - printed by Name (emp) <UTC time> - <project> Lot <unit> dwg <drawing> R<rev> - verify revision before use` and the stamp signature, in a white box at the bottom of the page.
- Stamp signature: 16 hex of HMAC-SHA256 with the seal key over the packet, the kind (a printed copy signs `print:<kind>`, so a print and an export of the same thing in the same minute never share a signature), the seal hash, the user and the time. A copy can be checked against the server with `verify_stamp`; from phase 9 the Admin Stamp check tab does it from a pasted line or a PDF.
- Stamp signature: 16 hex of HMAC-SHA256 with the seal key over the packet, the kind (a printed copy signs `print:<kind>`, so a print and an export of the same thing in the same minute never share a signature), the seal hash, the user and the time. A copy can be checked against the server with `verify_stamp`; from phase 9 the Admin Stamp check tab does it from a pasted line or a PDF, and from 2026-10-30 Q.C. and Clerical can too, on the Stamp check screen (Gitea issue 32, change C9).
- Logging (S7, S8): every print and export writes one `print_log` row (who, when, session, packet, unit or ITP, kind, what, pages, the stamp line, the stamp signature, the client address) and one audit row (`print` or `export`). `print_log` is append-only: the app role has SELECT and INSERT only, and a trigger refuses updates, deletes and truncate.
- CSV formula guard (S6): a cell that starts with `=`, `+`, `-`, `@`, a tab or a CR gets a leading `'`. A cell with a quote, a comma or a line break is quoted with the quotes doubled.
- PDF metadata: title, subject and keywords name the packet and the stamp signature; Author is PESCO and Producer is Packet Station.
@@ -167,10 +167,10 @@ The M: drive build had 20 layers of protection, most of them enforced in the bro
### Project Management, find bar and presence (phase 8)
- Project Management reads only. Every endpoint under `/api/pm` is a GET and needs a signed-in session. Any role may read them, as any role may open any packet. The PM role passes no write action anywhere. The reports read PostgreSQL only, never VISUAL or M-Files.
- Project Management reads only. Every endpoint under `/api/pm` is a GET and needs a PS-ProjectMgmt or Admin session; every other role is 403 (Gitea issue 33, change C10, built 2026-10-30; phase 8 let any signed-in role read them). The PM role passes no write action anywhere. The reports read PostgreSQL only, never VISUAL or M-Files.
- Every PM export is built on the server and logged: one `print_log` row (kind `report-csv` or `report-pdf`, no packet, the scope label in `what`, the page count for the PDF) and one audit row with the action `report exported` and the detail `{name, format, scope, period}`. `print_log` stays append-only.
- The PM CSV has the formula guard on every cell (S6), a BOM and CRLF line ends. The PDF report carries the controlled-copy stamp on every page and the PDF metadata. Both answer with `Content-Disposition`, `nosniff` and `no-store`, as the phase 7 exports do.
- Filters never widen access. The search, the status, flag, station, worker and Mine filters are applied by the server inside each list's and board's own scope (a supervisor's departments, the stall's station). A filter can only narrow what the person may already read.
- Filters never widen access. The search, the status, flag, station, worker and Mine filters are applied by the server inside each list's and board's own scope (a supervisor's departments, the stall's own assignments). A filter can only narrow what the person may already read.
- Search words are bound parameters, matched with ILIKE and the LIKE escape. A query keeps at most 8 words of at most 60 characters each. IDs are matched as text, never cast.
- Mine is decided on the server from the person's AD roles and the screen they name. Nothing the browser keeps can widen it.
- Presence shows the display name and the role only (the user id, screen, packet id and since travel with it). No employee ID, no email, no other personal data. The server builds it from the session, not from what the browser says about itself.
@@ -244,10 +244,21 @@ The account limit is capped at 15 so the app always stops before AD's own lockou
### Admin (phase 9)
- Every Admin endpoint needs PS-Admins in a password session. Every Admin change is audited with before and after. Tests try every endpoint as every role.
- Roles still come only from AD. The Admin edits departments (name, stations), profiles (stamp, profile active, station limits), checklists, the security settings and the notification rules. None of these grants a role. Turning a profile off hides the person from pickers and changes no right.
- Roles still come only from AD. The Admin edits departments (name, stations), profiles (stamp, profile active, station limits), the checklists (any department's, and the default lists by kind), the security settings and the notification rules. None of these grants a role. Turning a profile off hides the person from pickers and changes no right.
- Department codes and short names never change (a trigger, and 422 on the API). Stations are never deleted.
- The access log CSV has the formula guard, at most 50,000 rows, and is logged in `print_log` and the audit.
- The stamp check recomputes the stamp signature with the seal key and compares the text with the print log (S7, S8). It reads a pasted line or a PDF of up to 25 MB.
- The stamp check recomputes the stamp signature with the seal key and compares the text with the print log (S7, S8). It reads a pasted line or a PDF of up to 25 MB. From 2026-10-30 it is open to Q.C. and Clerical as well (see below).
### SethD's changes (built 2026-10-30)
- Stamp check (Gitea issue 32, change C9): `POST /api/stamp-verify` and `/api/stamp-verify/pdf` (and the `/api/admin` aliases) are for Admin, every Q.C. role and every Clerical role, in a password session. A PIN session is 403 `password_session_required`; every other role is 403. Each check is audited (`admin.stamp_verified`). The seal key never leaves the server, and the check never trusts the pasted text.
- Project Management (Gitea issue 33, change C10): every `/api/pm/*` endpoint, the CSV and PDF exports included, is for PS-ProjectMgmt and Admin only. Tests try every endpoint as Clerical, worker, Q.C., Warehouse and shop supervisor and expect 403.
- Department checklists (Gitea issue 23, change C8): any session reads them. A save needs that department's Supervisor or an Admin, in a password session (a PIN session is 403 `password_session_required`), with the version it started from. A lead, Q.C. and the office roles cannot save. Each save writes an audit row `checklists.dept_updated` with the list before and after. A stage keeps the list frozen at its START, so an edit cannot change what an open stage must tick.
- Short approvals (Gitea issue 18, change C7): the department supervisor approves each receipt with a signature. `short_approvals` is append-only (triggers, and `ps_app` has SELECT and INSERT only), one row per receipt by a unique key.
- Filing (Gitea issues 25 and 31, changes C3 and C5): FILE needs the unit's ITP complete and, on a VISUAL packet, the feed's own observation that VISUAL closed every work order. Clerical cannot mark a VISUAL packet closed by hand. Neither can be bypassed, Admin included.
- Q.C. (Gitea issue 24, change C2): a reject always needs a reason of 8 characters; the accept after a reject without an NCR number needs one, checked by pattern on the server.
- The data book without the history pages (D7; Gitea issue 8, change C6): the one data book goes to the customer and leaves out the packet log. The history stays in Packet Station, readable by anyone who can open the packet, and in the audit.
- The Stall screen (Gitea issue 36, change C11): `/api/stall/mine` lists only the packets assigned to the signed-in person. The request names no station; the session decides.
### Backups (phase 9)
@@ -298,7 +309,8 @@ The IDs come from Part 3 of the old SECURITY.md. The status follows the approved
| D1, D2, D4 | Deferred (envelope encryption, Key Vault) |
| D3 | IT's (host disk encryption on docker-1) |
| D5 | Closed, phase 4 (optional ClamAV: a `clamav` compose profile; when `CLAMAV_HOST` is set every upload and every stored drawing is scanned before it is kept, a hit is refused and audited, and an unreachable scanner refuses uploads; off when the host is not set; left off in both stacks by IT on 2026-10-18, Gitea issue 19, with Microsoft Defender on the workstations) |
| D6, D7 | Phase 10 (decided 2026-10-28, Gitea issues 7 and 8: nothing is purged, filed packets are archived to the QNAP share at filing, and the one data book leaves out the history pages) |
| D6 | Phase 10 (decided 2026-10-28, Gitea issue 7: nothing is purged, and filed packets are archived to the QNAP share at filing) |
| D7 | Closed 2026-10-30 (decided 2026-10-28, Gitea issue 8, change C6: the one data book, the customer copy, is built without the history pages; the history stays in Packet Station and the audit) |
| M1 | Closed, phase 2 (append-only audit and packet log written only by the server; every packet write carries both) |
| M2 | Closed, phase 7 (filing seal is HMAC-SHA256 with the server seal key from a Docker secret; a reopen keeps the broken seal and its data book) |
| M3 | Closed, phase 9 (the nightly `integrity` job re-verifies seals, rehashes asset files, finds missing and orphan files and walks the audit hash chain; findings are kept in the append-only `integrity_findings` and raise `integrity_failure`, which nobody can opt out of) |