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
+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) |