Phase 6: Q.C. inspection, project folders, packet documents and the digital ITP
The Q.C. screen (KPIs, boards, project folders), the Q.C. tab on the packet page (check items from the qc_check_items settings row, visual exam per weld, NDE, hardness with the NACE label, notes), accept with a route and a signature gated by qc_missing_empty, reject with a reason or an NCR number closing the inspection into qc_history, Q.C. folder documents per project and unit with superseding revisions, packet documents as a closed set with take-out reasons, the digital ITP per unit from the frozen template (line initials with CAS, unsign, N/A, customer codes, rejects with NCR, block signatures, sign-off with the PDF hook, admin reopen, the ITP log), hold-point warnings by department page group, the scoped policy check for unit and project writes, migration 0007, tests, Playwright scenarios and docs. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -251,6 +251,14 @@ From phase 5 (2026-10-16), for Seth to veto:
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
## Verification
|
||||
|
||||
- Every phase: `pytest` server suite, `npm test` and `npm run build` for the frontend, `docker compose -f deploy/compose.ci.yml up` with the Samba DC and the Playwright scenarios for that phase, `packetstation doctor` inside the image.
|
||||
|
||||
+207
-9
@@ -4,7 +4,7 @@ Who may do what to a packet, and when. This file is written from the code in `se
|
||||
|
||||
## Purpose
|
||||
|
||||
The policy is a Python port of the old `guard.js`. It runs only on the server. Every packet write calls `policy.check()` inside the write transaction, under the packet row lock. Every packet read returns `can`, `blocked` and `holder` from the same rules, so the screens only show what the server will accept.
|
||||
The policy is a Python port of the old `guard.js`. It runs only on the server. Every packet write calls `policy.check()` inside the write transaction, under the packet row lock. Every packet read returns `can`, `blocked` and `holder` from the same rules, so the screens only show what the server will accept. The Q.C. folder and ITP writes belong to a project or a unit, not to a packet; they call `policy.check_scoped()` under their own row lock (see "Scoped checks").
|
||||
|
||||
The package is pure functions with no I/O:
|
||||
|
||||
@@ -12,7 +12,7 @@ The package is pure functions with no I/O:
|
||||
- `principal.py`: `Principal`, `PacketState`, `principal_from_roles()`.
|
||||
- `custody.py`: `holds()`, `works_on()`, `holder_name()` and the Who predicates.
|
||||
- `preconditions.py`: the named preconditions and their messages.
|
||||
- `engine.py`: `check()`, `evaluate()`, `can()` and `Decision`.
|
||||
- `engine.py`: `check()`, `check_scoped()`, `evaluate()`, `can()` and `Decision`.
|
||||
|
||||
## Roles
|
||||
|
||||
@@ -101,8 +101,6 @@ 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 | | |
|
||||
|
||||
A holder of the packet who is not in the warehouse receives a short through edit_bom (see "Receive by custody").
|
||||
| edit_heats | HOLDER | warehouse, SHOP | | | | | | |
|
||||
| edit_heat_table | HOLDER | warehouse, SHOP | | | | | | |
|
||||
| edit_welds | HOLDER | SHOP | | | | | | |
|
||||
@@ -123,17 +121,28 @@ A holder of the packet who is not in the warehouse receives a short through edit
|
||||
| co_reject | CL | any | | | | reason_given | Reject a change order | yes |
|
||||
| co_hold_toggle | CL | any | | | | | | |
|
||||
| co_verify | CO_VERIFIER | any | | | | | Verify a change order for the department | |
|
||||
| itp_edit_line | QC | any | | | | | | |
|
||||
| itp_block_sign | QC | any | | | | | Sign an ITP block | |
|
||||
| itp_sign_off | QC | any | | | | | Sign off the ITP | |
|
||||
| itp_reopen | QC_SUP (Admin through the bypass) | any | | | | reason_given | | yes |
|
||||
| qc_doc_add | CL_OR_QC | no packet (scoped) | | | | | | |
|
||||
| itp_start | QC | no packet (scoped) | | | | | | |
|
||||
| itp_line_sign | QC | no packet (scoped) | | | | | | |
|
||||
| itp_line_unsign | QC | no packet (scoped) | | | | | | |
|
||||
| itp_edit_line | QC | no packet (scoped) | | | | | | |
|
||||
| itp_block_sign | QC | no packet (scoped) | | | | | Sign an ITP block | |
|
||||
| itp_block_unsign | QC | no packet (scoped) | | | | | | |
|
||||
| itp_sign_off | QC | no packet (scoped) | | | | | Sign off the ITP | |
|
||||
| itp_reopen | QC_SUP (Admin through the bypass) | no packet (scoped) | | | | reason_given | | yes |
|
||||
| vis_update_draft | SYSTEM | draft | | | | | | |
|
||||
| vis_retire_draft | SYSTEM | draft | | | | | | |
|
||||
| vis_open_co | SYSTEM | any but draft and filed | | | | | | |
|
||||
| vis_auto_release | SYSTEM | draft | warehouse | | | heat_map_answered, drawing_attached | | |
|
||||
|
||||
A holder of the packet who is not in the warehouse receives a short through edit_bom (see "Receive by custody").
|
||||
|
||||
The `vis_*` actions are system only. People can never do them, and the worker can do nothing else.
|
||||
|
||||
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").
|
||||
|
||||
## Preconditions
|
||||
|
||||
Each precondition is a named predicate over the packet state and the request input. They are never bypassed, Admin included. The first one that fails gives the message.
|
||||
@@ -176,6 +185,17 @@ Each precondition is a named predicate over the packet state and the request inp
|
||||
|
||||
The packet API answers 403 with the code for most failures, and 400 for `it_reason_required` and `reason_required`.
|
||||
|
||||
### Scoped checks
|
||||
|
||||
`check_scoped(principal, action, *, extra=None, it_reason=None) -> Decision` runs the same steps as `check()` without a packet, for the actions that belong to a project or a unit (the Q.C. folder and the ITP):
|
||||
|
||||
1. **System.** A system principal fails (`forbidden`). A system-only action fails every person (`system_only`).
|
||||
2. **Admin bypass.** An admin not allowed by the rule's Who passes with an IT reason of at least 8 characters, else `it_reason_required`. The decision is marked `admin_bypass`.
|
||||
3. **Who.** Code `forbidden`, "Your access doesn't allow this."
|
||||
4. **Preconditions,** the input ones in rule order. `reason_given` fails as `reason_required`.
|
||||
|
||||
There is no removed, status, change-order hold, filed lock or custody step: there is no packet and no holder. The ITP and Q.C. folder writes call it inside their own transaction, under the `itps` row lock or the lock on the Q.C. document being replaced. They never take a packet row lock, because they change no packet and must not wait on, or block, the packets of the unit.
|
||||
|
||||
## 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.
|
||||
@@ -190,7 +210,8 @@ The request carries `sign: {factor, secret}`. The write pipeline calls the phase
|
||||
- **Header scalars** use field-level compare-and-set: the body sends `set` and `base`. A key whose base differs from the current value is 409 `field_conflict` with the current value. A key with no base is written as is.
|
||||
- **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".
|
||||
- **Append-only tables** never conflict: packet_transitions, packet_log, qc_history, short_receipts, project_renumbers, itp_rejects and co_verifications. Triggers refuse UPDATE, DELETE and TRUNCATE, and the app role has no grant for them.
|
||||
- **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 and co_verifications. Triggers refuse UPDATE, DELETE and TRUNCATE, and the app role has no grant for them.
|
||||
|
||||
## Reads: can, blocked and holder
|
||||
|
||||
@@ -433,6 +454,175 @@ The `action` and `note` columns of `packet_log` for each shop write. A departmen
|
||||
|
||||
The submit hours are the stage's `clock_elapsed_ms` after the stop, with one decimal.
|
||||
|
||||
## Q.C. (phase 6)
|
||||
|
||||
### Who inspects
|
||||
|
||||
Q.C. holds a packet in status qc. EDIT_QC (QC, status qc) covers every write to the open inspection: the check items, the visual result per weld, the NDE fields, hardness, the inspector name, the stamp and the notes. A write with no open inspection is 409 `no_inspection`.
|
||||
|
||||
| Write | Endpoint | Concurrency |
|
||||
|---|---|---|
|
||||
| One check item | `PUT /api/packets/{id}/qc/checks/{key}` `{done}` | one key by `jsonb_set` under the packet row lock, no If-Match. Unknown key 422 `invalid_key`. |
|
||||
| One weld's visual result | `PUT /api/packets/{id}/qc/welds/{no}` `{result?, note?}` | one weld by `jsonb_set`. The weld must be live on the packet (422 `invalid_weld`). `result` is `accept`, `reject` or empty. |
|
||||
| The other fields | `PATCH /api/packets/{id}/qc` `{set, base}` | field-level compare-and-set over `inspector`, `stamp`, `nde_method`, `nde_result`, `nde_report`, `hardness`, `notes`; 409 `field_conflict` with the current value. |
|
||||
|
||||
`nde_method` must be one of `None required`, `RT`, `UT`, `MT`, `PT` (422 `invalid_field` "Pick an NDE method from the list."). `nde_result` is empty, `Acceptable` or `Rejectable`. Setting the method to `None required` clears the result and the report number. The phase 2 flat body (with `If-Match` on the inspection's `row_version`, merging `checks` and `weld_exam`) still works; a body with `set` selects the field compare-and-set. The NDE rules apply to both. Hardness is labelled `Hardness (NACE)` when the packet's WPS contains `NACE`. The server computes this; no request sets it.
|
||||
|
||||
### Accept
|
||||
|
||||
Two actions accept. Both are Q.C. from status qc, held by an open change order, gated by `qc_missing_empty`, and signed.
|
||||
|
||||
| Route | Action | Body | Result |
|
||||
|---|---|---|---|
|
||||
| Clerical – Data Book | qc_accept | `routed_to: "databook"` | complete, routed to the data book |
|
||||
| Shipping | qc_accept | `routed_to: "shipping"` | complete, routed to Shipping |
|
||||
| A shop department | qc_accept_and_route | `dept: "09"` | ready in that department, a new stage opened |
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
| Part | When |
|
||||
|---|---|
|
||||
| `3 inspection items` | that many check items are not done |
|
||||
| `2 welds without a visual result` | that many live welds have no accept or reject |
|
||||
| `a rejected weld (reject the packet instead)` | any weld is rejected |
|
||||
| `NDE result` | an NDE method other than `None required` is set and the result is not `Acceptable` |
|
||||
| `inspector name` | the inspector is empty |
|
||||
|
||||
### 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:
|
||||
|
||||
- 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 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.
|
||||
|
||||
The change-order hold applies to both accepts and to file. It does not apply to the reject: Q.C. can always send a packet back.
|
||||
|
||||
The NCR link is `https://ncr.pescoinc.biz/ncrs/<number>`. `domain/ncr.py` `validate_ncr` is the hook for checking the number against pesco-ncr. Today it accepts any well-formed number. The check against pesco-ncr is a later phase.
|
||||
|
||||
### Packet documents
|
||||
|
||||
DOC_ADD and DOC_REMOVE are Clerical or Q.C. (CL_OR_QC), in any status but filed (the filed lock applies).
|
||||
|
||||
- `POST /api/packets/{id}/docs` `{asset_id, kind, title?, note?}`. `kind` is one of the nine kinds (see [SCHEMA.md](SCHEMA.md), "Packet documents"); any other is 422 `invalid_kind` "Pick a document type from the list." The asset must be a PDF, PNG or JPEG (422 `invalid_asset`). The title defaults to the kind's label and is up to 160 characters. The note is up to 300 characters.
|
||||
- `DELETE /api/packets/{id}/docs/{doc_id}` `{reason}` takes a document out of the data book. The reason needs 3 characters after cleaning (400 `reason_required` "Say why it comes out of the data book."). The row is kept with who, when and why. A second take-out is 409 `already_removed`.
|
||||
|
||||
Drawings are never packet documents. They come from M-Files or the hand attach only.
|
||||
|
||||
### Q.C. folder documents
|
||||
|
||||
QC_DOC_ADD is Clerical or Q.C. (CL_OR_QC), checked with `check_scoped()`. `POST /api/qc/docs` `{project_id | unit_id, type, title?, rev?, note?, asset_id, replaces?, it_reason?}`:
|
||||
|
||||
- Exactly one of `project_id` and `unit_id` (422 `invalid_request`).
|
||||
- `type` is one of `ITP`, `WPS / PQR`, `NDE procedure`, `Hydrotest procedure`, `PMI / material`, `Calibration certificate`, `Customer spec`, `Other` (422 `invalid_type` "Pick a document type from the list.").
|
||||
- The asset must be a PDF, PNG or JPEG (422 `invalid_asset`). Limits: title 140 (defaults to the type), rev 12, note 600.
|
||||
- `replaces` files a new revision. The replaced row must be a current document of the same folder (422 `invalid_replace`). One that already points at a newer row is 409 `already_superseded`. The old row is kept and points at the new one. Nothing is deleted.
|
||||
|
||||
Reads (`GET /api/qc/home`, `/api/qc/folders`, the project and unit folder reads) need any signed-in session. PM reads and changes nothing.
|
||||
|
||||
Clerical may add Q.C. folder documents. The old build allowed Q.C. and Admin only. This is wider than the old build; Seth can narrow it to QC.
|
||||
|
||||
### The ITP
|
||||
|
||||
Each unit has at most one digital ITP, started from the code template `ITP_Generation_Revised_2` and frozen at start. Reads (`GET /api/units/{unit_id}/itp`) need any session. Every write runs `check_scoped()` with its action, then finds the ITP (404), then refuses a complete one (409 `itp_complete`), locks the `itps` row, bumps `itps.version` once, and writes one `itp_log` row per change, one audit row for each (`itp.<action value>`, so `itp.itp_start`, `itp.itp_line_sign`; subject `itp`; detail `{text, note, unit_id}`, plus `signature_id` on a signed one) and a change event, in one transaction. Block sign and sign-off check their gates before the signature, so a refused step costs no PIN attempt, and check them again under the lock.
|
||||
|
||||
Every line and block row is made at start. A Q.C. user's packet read still lists the scoped actions in `can`; they mean nothing on a packet.
|
||||
|
||||
| Action | Who | Endpoint | Gate |
|
||||
|---|---|---|---|
|
||||
| itp_start | QC | `POST /api/units/{unit_id}/itp` | 409 `itp_exists` when the unit has one |
|
||||
| itp_edit_line | QC | `PATCH /api/itps/{id}` (header), `PATCH .../lines/{key}`, `POST .../sections/{n}/na`, `POST .../lines/{key}/reject`, `POST .../lines/{key}/ncr`, `PATCH .../blocks/{key}` | see below |
|
||||
| itp_line_sign | QC | `POST /api/itps/{id}/lines/{key}/sign` | the line is not approved (409 `already_signed`) or N/A (409 `line_na`); a rejected line needs its NCR (409 `ncr_required` "Enter the NCR # before approving this line.") |
|
||||
| itp_line_unsign | QC | `POST /api/itps/{id}/lines/{key}/unsign` | the line is approved (409 `not_signed`); only the inspector who initialed it, or an admin (403 `forbidden` "Only the inspector who initialed this line, or an admin, can remove it."); refused while the block is signed (409 `block_signed` "Undo the section signature first.") |
|
||||
| itp_block_sign | QC, signature "Sign an ITP block" | `POST /api/itps/{id}/blocks/{key}/sign` | every line of the block done (409 `block_not_ready` "Every line must be initialed or N/A first."); not already signed (409 `already_signed`) |
|
||||
| itp_block_unsign | QC | `DELETE /api/itps/{id}/blocks/{key}/sign` | the block is signed (409 `not_signed`); the signer or an admin (403 `forbidden` "Only the inspector who signed this block, or an admin, can remove it."); clears the signature and keeps the block's fields |
|
||||
| itp_sign_off | QC, signature "Sign off the ITP" | `POST /api/itps/{id}/signoff` | every block signed (409 `blocks_unsigned` "Every section must be signed first.") |
|
||||
| itp_reopen | QC_SUP, or an admin through the bypass | `POST /api/itps/{id}/reopen` `{reason, it_reason?}` | a reason of 8 characters; the ITP is complete (409 `itp_open` "The ITP is open already."). An admin who gives no `it_reason` has the reason used as both. |
|
||||
|
||||
Line approval is a compare-and-set: the `itps` row lock, the state check above, and an optional `If-Match: <line row_version>` (409 `row_conflict`). Approval sets `result approve`, `by`, `at` and the initials (the first letter of each word of the display name, upper case, at most three). Unsign clears the result, the approver, the time and the initials, so the line is open again. The line's `ncr` and every `itp_rejects` row stay.
|
||||
|
||||
The itp_edit_line writes:
|
||||
|
||||
- Header (`no`, `client`, `pname`, `pno`, `po`, `unit`): field compare-and-set with `{set, base}` (409 `field_conflict`), 120 characters each. `no` cannot be empty (422 `invalid_field`).
|
||||
- Line details: `na` is refused on an approved line (409 `already_signed`) or one with an open reject (409 `line_rejected`). `cust_code` is one of `R`, `R/AU`, `AU`, `W`, `H`, `NA`, or empty for the template's (422 `invalid_field` "Pick a customer code from the list."). `cust_by` 80, `note` 500. `cust_at` defaults to today when a name is given. A change that changes nothing writes no log and no version.
|
||||
- Section N/A: sets or clears N/A on every line of the section. Marking N/A is refused when any line of the section is approved (409 `already_signed`) or has an open reject (409 `line_rejected`).
|
||||
- Reject: refused on a line that is approved (409 `already_signed`), N/A (409 `line_na`) or already rejected (409 `line_rejected`). The NCR is optional and validated when given. An `itp_rejects` row (kind `reject`) is added.
|
||||
- NCR entered later: the line must have an open reject without an NCR (409 `no_open_reject`). An `itp_rejects` row (kind `ncr`) is added.
|
||||
- Block fields: `qn` 40, `remarks` 400, `cust_name` 80, `cust_at` (today when a name is given), `cust_clear` clears the customer entry. An empty `cust_name` without `cust_clear` is 422 `invalid_field` "Enter the customer rep's name." No signature.
|
||||
|
||||
A write to a complete ITP, other than the reopen, is 409 `itp_complete` "The ITP is signed off and locked. An admin can reopen it." The sign-off sets status `complete` and calls the phase 7 hook `pdf.itp.file_signed_itp_pdf()`. Today the hook returns nothing. When it returns an asset, the sign-off sets `signoff_asset_id` and files a `qc_docs` row on the unit (type `ITP`, `auto`), superseding any earlier automatic ITP document of the unit. A reopen sets status `open` with the reason. Block and line signatures stay; the inspector undoes what must change.
|
||||
|
||||
ITP actions never look at a packet. A filed packet in the unit does not lock the ITP, because the ITP belongs to the unit (the phase 5 follow-up, open question 28).
|
||||
|
||||
### Blocks
|
||||
|
||||
A signature block closes at every template section marked to sign, and covers the sections since the last block. The template gives blocks `3` (sections 1 to 3), `4`, `5`, `6`, `7`, `8` and `9` (the final block). A line is done when it is N/A or approved. A block is ready when every line in it is done, so a block of N/A lines only can be signed.
|
||||
|
||||
### Hold points
|
||||
|
||||
A hold code is `H` or `W`, alone or after a slash (`R/H`), on the PESCO code or the customer code (the line's own customer code when set, else the template's). An open hold point is a line with a hold code that is not done, or any rejected line.
|
||||
|
||||
The packet read gives `itp_hold` while the unit's ITP is open: the open hold and rejected points, filtered by the packet's department page group. It is a warning only. No precondition reads it, and no step is blocked by it. Q.C. and the shop check with each other before moving past a point.
|
||||
|
||||
| Department | Pages it sees |
|
||||
|---|---|
|
||||
| 01, 02, 03, 04, 05, 10 | WP (Weld Parts) |
|
||||
| 06, 07, 13 | C (Code) |
|
||||
| 09 | WP and C |
|
||||
| 08, 11, 12, 14 | FA (Final Assembly) |
|
||||
| CL, WH, QC, or an unknown code | every page |
|
||||
|
||||
### Log texts
|
||||
|
||||
The `action` and `note` of `packet_log` for each Q.C. packet write.
|
||||
|
||||
| Write | action | note |
|
||||
|---|---|---|
|
||||
| Check ticked | `Q.C. checked: <item text>` | |
|
||||
| Check unticked | `Q.C. unchecked: <item text>` | |
|
||||
| Visual result set | `VT accept · W3`, `VT reject · W3` | the note when given |
|
||||
| Visual result cleared | `VT cleared · W3` | |
|
||||
| Weld note only | `Q.C. record changed` | `W3 note` |
|
||||
| NDE method | `NDE method: RT`, `NDE method: -` when cleared | |
|
||||
| NDE result | `NDE result: Acceptable`, `NDE result: -` when cleared (no line of its own when `None required` cleared it) | |
|
||||
| Other Q.C. fields | `Q.C. record changed` | the changed fields, `inspector Quinn QC · stamp Q1 · hardness 198 HB · report RPT-1 · notes changed` (`<field> cleared`, `notes cleared`) |
|
||||
| Flat-body checks or weld exam merge | `Q.C. record changed` | |
|
||||
| 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` |
|
||||
| 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>` |
|
||||
|
||||
One PATCH that changes several kinds of field writes one line for each. The arrow and the en dash in `Clerical – Data Book` are the old build's texts. The stamp shows in brackets only when set. The inspector is the inspection's `inspector` field.
|
||||
|
||||
The `action` and `note` of `itp_log` (and the audit row's `detail.text`) for each ITP write. Dates are `M/D/YYYY` in America/Denver.
|
||||
|
||||
| Write | action | note |
|
||||
|---|---|---|
|
||||
| Start | `ITP started` | `L073028-ITP · template ITP_Generation_Revised_2` |
|
||||
| Header field | `ITP header changed`; `ITP plan no. changed` for `no` | `client: ACME`; the new number for `no` |
|
||||
| Line approved | `ITP line 1.3 approved` | `after NCR NCR-2026-0021` when the line was rejected before |
|
||||
| Line unsigned | `ITP line 1.3 initial removed` | `was Quinn QC 10/17/2026` |
|
||||
| Line details | `ITP line 1.3 updated` | the changes joined by `, `: `marked N/A`, `applies again`, `customer code W`, `customer code as template`, `customer Rick Inspector`, `customer cleared`, `customer date 10/17/2026`, `note` |
|
||||
| Line rejected | `ITP line 1.3 rejected` | `NCR NCR-2026-0021 · <note>` or `NCR # pending · <note>` |
|
||||
| NCR entered later | `ITP line 1.3 NCR number entered` | `NCR NCR-2026-0021` |
|
||||
| Section N/A | `ITP section 5 marked N/A`, `ITP section 5 applies again` | the section title |
|
||||
| Block fields | `ITP block 3 QN # changed`, `ITP block 3 remarks changed` | the value |
|
||||
| Block signed | `ITP block 3 signed` | `Sections 1–3` or `Section 4` |
|
||||
| Block unsigned | `ITP block 3 signature removed` | |
|
||||
| Customer complete | `ITP block 3 customer complete recorded`, `ITP block 3 customer complete cleared` | `Rick Inspector` (the cleared name on a clear) |
|
||||
| Sign-off | `ITP signed off` | `PDF filed` when the hook filed one, else `PDF filing reserved for phase 7` |
|
||||
| Reopen | `ITP reopened` | the reason |
|
||||
|
||||
A Q.C. folder document writes an audit row `qc_doc.add` or `qc_doc.revise` (subject `qc_doc`), with `doc_id`, `project_id`, `unit_id` and `replaces` in the detail, `detail.text` `Q.C. document added` or `Q.C. document revised` and `detail.note` `L670 · WPS / PQR · WPS-101 GTAW/SMAW Rev 2 · wps.pdf`.
|
||||
|
||||
## Worker writes (phase 3)
|
||||
|
||||
The worker has no session. It writes through the same policy as a person, as the system principal `Principal(user_id=None, system="visual-feed")`. Each write locks the packet row (`FOR UPDATE SKIP LOCKED`; a row locked by someone else is left for the next run), builds the packet state, runs `policy.check()`, makes its change, bumps `version`, and writes a `packet_log` row (by nobody, by name "VISUAL feed", role `system`), an audit row (`packet.<action>`, role `system`) and a change event. See [FEED.md](FEED.md) for when each write happens.
|
||||
@@ -504,3 +694,11 @@ The old policy was looser than the old screens in places. The rebuild follows th
|
||||
| The old screen asked for a weld of the current department before submit, but the policy did not check welds. | welds_recorded counts the welds of the packet's current department, on the server. After a route the new department marks its own. | Each department records its own welds. | Yes |
|
||||
| A mark could be placed on a VOID sheet. | A VOID sheet refuses new marks and moves (422 `void_page`). The marks already there stay. | Marks belong on the current revision. | Yes |
|
||||
| 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). | A short note like "no" says nothing to the supervisor. An NCR number points to the full record. | Yes |
|
||||
| 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 |
|
||||
|
||||
+87
-13
@@ -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 9 adds the notification rules and delivery. 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 9 adds the notification rules and delivery. The DDL is in `server/packetstation/migrations/`; this file explains it. See [PLAN.md](PLAN.md), "Schema".
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -46,9 +46,9 @@ Declaration order matters for `packet_status`: it is the workflow order, so `sta
|
||||
| `project_status` | open, hold, closed |
|
||||
| `route_kind` | databook, shipping |
|
||||
| `drawing_state` | none, needed, attached, rev_mismatch, not_released, not_found, lookup_failed, hand |
|
||||
| `itp_status` | open, signed, reopened |
|
||||
| `itp_status` | open, signed, reopened, complete (0007) |
|
||||
|
||||
Smaller closed sets are text with a CHECK: `projects.source` and every `*_src` (visual, hand), `project_aliases.kind` (renumber, unit_renumber, import), `packet_pages.type` (drawing, info, overlay), `bom_lines.src` (visual, hand), `qc_inspections.result` (accepted, rejected), `itp_lines.result` (approve, reject, na). Null means unset in each.
|
||||
Smaller closed sets are text with a CHECK: `projects.source` and every `*_src` (visual, hand), `project_aliases.kind` (renumber, unit_renumber, import), `packet_pages.type` (drawing, info, overlay), `bom_lines.src` (visual, hand), `qc_inspections.result` (accepted, rejected), `itp_lines.result` (approve, reject, na). Migration 0007 adds `packet_docs.kind`, `qc_docs.type` and `itp_rejects.kind` (see "Q.C. (phase 6, migration 0007)"). Null means unset in each.
|
||||
|
||||
## Projects and units (phase 2)
|
||||
|
||||
@@ -101,7 +101,7 @@ What the warehouse columns mean in practice (phase 4):
|
||||
- `heat_table_placements`: `page`, `x`, `y`, `s`, `carried_from` (0006), `by`, `at`. Unique on (`packet_id`, `page`), written as an upsert.
|
||||
- `packet_wh`: the warehouse stage history. `stage` (wh_stage), `by`, `at`, `reason`, `note`, `auto`, `closed_at`. The current row has `closed_at` null and is unique per packet. A stage change closes it and inserts the next.
|
||||
- `packet_transitions`: append-only. Every status change: `at`, `by`, `action`, `from_status`, `to_status`, `from_dept`, `to_dept`, `reason`, `signature_id`, `it_reason`. Replaces the old `hist` and `sentBack` lists.
|
||||
- `qc_inspections`: `stage_id`, `sent_at`, `sent_by`, `book`, `spool`, `location`, `sent_note`, `checks` jsonb, `weld_exam` jsonb (weld_no to `{result, note}`), `nde_method`, `nde_result`, `nde_report`, `hardness`, `nace`, `inspector`, `stamp`, `notes`, `result`, `result_at`, `result_by`, `result_note`, `signature_id`, `row_version`, `closed_at`. At most one open inspection per packet.
|
||||
- `qc_inspections`: `stage_id`, `sent_at`, `sent_by`, `book`, `spool`, `location`, `sent_note`, `checks` jsonb, `weld_exam` jsonb (weld_no to `{result, note}`), `nde_method`, `nde_result`, `nde_report`, `hardness`, `nace`, `inspector`, `stamp`, `notes`, `result`, `result_at`, `result_by`, `result_note`, `signature_id`, `row_version`, `closed_at`, `ncr` (0007). At most one open inspection per packet. See "The Q.C. inspection".
|
||||
- `qc_history`: append-only. `at`, `result`, `inspector`, `note`, `dept_code`, `snapshot` jsonb.
|
||||
- `packet_seals`: the HMAC-SHA256 `hash` written at filing, `key_id`, `sealed_at`, `sealed_by`, `signature_id`, and `broken_at`, `broken_by`, `broken_reason` when IT reopens. At most one unbroken seal per packet.
|
||||
- `packet_log`: append-only. The user-visible history: `at`, `by`, `by_name` and `role` (snapshots), `action`, `note`, `it_reason`, `signature_id`, `transition_id`. Indexed on (`packet_id`, `id`).
|
||||
@@ -190,6 +190,50 @@ A clock op in the wrong state is 409 `clock_state`.
|
||||
|
||||
The read model gives `server_now` (the database `now()`) and `stage.clock`: `{elapsed_ms, stored_ms, running, running_since, started_at}`. `stored_ms` is the column. `elapsed_ms` is live: the stored total plus `now() - clock_running_since` while running. Each closed stage has the same object, with `running` false. The client adds the time since the fetch to `elapsed_ms` for the ticking display only.
|
||||
|
||||
## Q.C. (phase 6, migration 0007)
|
||||
|
||||
### Column additions
|
||||
|
||||
- `qc_inspections`: `ncr` (text). The NCR number given with a Q.C. reject, upper case (`NCR-2026-0021`).
|
||||
- `packet_docs`: `note` (text, up to 300 characters). `kind` gains a CHECK: one of `nde`, `hydro`, `dimensional`, `pmi`, `coating`, `coc`, `customer`, `ncr`, `other`.
|
||||
- `qc_docs`: `title`, `note`, `superseded_at` and `superseded_by_user` (the person who filed the newer revision). Two CHECKs: `qc_docs_scope` (exactly one of `project_id` and `unit_id` is set) and `qc_docs_type` (one of `ITP`, `WPS / PQR`, `NDE procedure`, `Hydrotest procedure`, `PMI / material`, `Calibration certificate`, `Customer spec`, `Other`; the label itself is stored). Indexes `qc_docs_project` and `qc_docs_unit` on the current rows (`superseded_by` null).
|
||||
- `itp_status` gains `complete`. A signed-off ITP is `complete`. `signed` and `reopened` stay in the enum unused: PostgreSQL cannot drop an enum value, and a reopened ITP is `open` again with `reopen_*` set.
|
||||
- `itps`: `no` (the ITP number, `<unit number>-ITP` at start), `hdr` jsonb (the header: `client`, `pname`, `pno`, `po`, `unit`), `created_at`, `created_by`.
|
||||
- `itp_lines`: `initials` (from the inspector's display name at approval), `cust_code` (the customer code for this line, null means the template's), `cust_by` (the customer witness or reviewer, as typed), `cust_at` (date), `cust_rec` and `cust_rec_at` (who recorded the customer entry, and when).
|
||||
- `itp_rejects`: `ncr` is now nullable (a reject can wait for its NCR number). `kind` (text, `reject` or `ncr` by CHECK, default `reject`): `reject` is the reject itself, `ncr` is the NCR number entered later.
|
||||
- `itp_blocks`: `signed_at` is now nullable with no default, so a block row can hold its fields before it is signed. New: `qn` (the QN number), `remarks`, `cust_name`, `cust_at` (date), `cust_by` and `cust_rec_at` (the customer complete entry and who recorded it), `row_version`.
|
||||
- `itp_log` (new, append-only): `itp_id`, `at`, `by`, `by_name` (snapshot), `action`, `note`, `signature_id`, `it_reason`. Indexed on (`itp_id`, `id`). The user-visible history of an ITP, as `packet_log` is for a packet.
|
||||
|
||||
`ALTER TYPE ... ADD VALUE` runs inside the migration's transaction. That is allowed because 0007 does not use the new value itself.
|
||||
|
||||
### Q.C. check items
|
||||
|
||||
The `settings` row `qc_check_items` holds the Q.C. check items. Migration 0007 inserts the defaults with `ON CONFLICT DO NOTHING`, so a row already there is kept. The value:
|
||||
|
||||
```json
|
||||
{"groups": [
|
||||
{"group": "Data book package", "items": [{"key": "db_heats", "text": "Heat number recorded for every BOM item"}, ...]},
|
||||
{"group": "Physical spool", "items": [{"key": "ph_dim", "text": "Dimensions verified against {drawing} Rev {rev}"}, ...]}
|
||||
]}
|
||||
```
|
||||
|
||||
| Group | Keys |
|
||||
|---|---|
|
||||
| Data book package | `db_heats`, `db_mtr`, `db_weldmap`, `db_wps`, `db_ck` |
|
||||
| Physical spool | `ph_dim`, `ph_heat`, `ph_stamp`, `ph_clean`, `ph_qty` |
|
||||
|
||||
The texts may hold `{wps}` (else `the WPS`), `{stamp}` (`[75] ` with a trailing space, or empty), `{drawing}` (else `the drawing`) and `{rev}` (else `-`), filled from the packet. When the row is missing or malformed, the defaults in `domain/checklists.py` apply and a warning is logged. No screen edits the row in phase 6.
|
||||
|
||||
`qc_inspections.checks` maps each key to `{done: true, at, by, stamp}` (the inspection's stamp) or `{done: false}`, written one key at a time with `jsonb_set`. A key not in the list is 422 `invalid_key`. `qc_inspections.weld_exam` maps a weld number to `{result, note, at, by}`, where `result` is `accept`, `reject` or empty. `nde_method` is one of `None required`, `RT`, `UT`, `MT`, `PT`; `nde_result` is empty, `Acceptable` or `Rejectable`. `nace` is no longer written: the NACE label is computed from the packet's WPS.
|
||||
|
||||
### The Q.C. inspection
|
||||
|
||||
- SEND_TO_QC (from review) and SEND_DRAFT_TO_QC (from draft) open a `qc_inspections` row with `book`, `spool`, `location`, `sent_note` and the stage. Each opens a fresh row with empty checks and exam.
|
||||
- At most one open row per packet (`qc_inspections_open`, unique on `packet_id` where `closed_at` is null).
|
||||
- QC_ACCEPT and QC_ACCEPT_AND_ROUTE set the result and close the row into `qc_history`. The stage is closed with the inspection as `qc_snapshot`.
|
||||
- QC_REJECT sets the result, the reason as `return_note` and the `ncr`, and closes the row into `qc_history` at once. The packet goes to review with no open inspection. A second SEND_TO_QC opens a new row; the first stays in `qc_history`.
|
||||
- A `qc_history` row keeps `at`, `result`, `inspector`, `note` (the result note) and `dept_code` as columns, and the full inspection row in `snapshot` (checks, weld exam, NDE fields, `ncr`, `result_by`, location, sent time).
|
||||
|
||||
## Change orders (phases 2 and 3)
|
||||
|
||||
- `change_orders`: `co_no` (from `next_co_no`; unique per packet), `origin` (co_origin), `review` (co_review), `minor`, `hold`, `held_by`, `held_at`, `ecn`, `reason`, `issued_*`, `approved_*` and `rejected_*` (each with a `*_signature_id`), `vis`, `waiting` and `staged` jsonb (feed-shaped), `target_fp`, `row_version`. At most one pending VISUAL change order per packet (unique on `packet_id` where origin is visual and review is pending), so the feed re-aims it instead of stacking new ones.
|
||||
@@ -200,6 +244,8 @@ The read model gives `server_now` (the database `now()`) and `stage.clock`: `{el
|
||||
|
||||
## Documents (phases 2, 4 and 6)
|
||||
|
||||
Nothing in this section is ever deleted. A packet document comes out of the data book by soft removal. A Q.C. folder document is replaced by a newer row that the old one points at. ITP rows are corrected forward, and the history is in the append-only `itp_rejects` and `itp_log`.
|
||||
|
||||
- `assets`: `id` uuid, `sha256` (indexed, for dedupe), `size`, `mime`, `original_name`, `storage_key` (the path under ASSETS_DIR, unique), `source` (`upload` by default), `source_ref` jsonb, `pages`, `uploaded_by`, `uploaded_at`, `packet_id` (the packet it was uploaded for, without a foreign key; `packet_pages` and `packet_docs` are the real links).
|
||||
|
||||
Asset kinds (phase 4). An upload names its kind in the `purpose` field, and `source_ref` keeps `{"purpose": kind}`. No purpose means `document`. The type is read from the content, never from the name or the browser.
|
||||
@@ -212,25 +258,53 @@ Asset kinds (phase 4). An upload names its kind in the `purpose` field, and `sou
|
||||
| bom | XLSX, CSV | 5 MB (`upload_max_bytes_bom`) |
|
||||
|
||||
`mime` is one of `application/pdf`, `image/png`, `image/jpeg`, `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` and `text/csv`. An XLSX is a zip that holds `[Content_Types].xml` and `xl/workbook.xml`; a plain zip is refused. A CSV is UTF-8 text with no NUL and no control character other than tab, CR and LF. An upload the virus scanner refuses is never stored, so it has no row.
|
||||
- `packet_docs`: `asset_id`, `kind`, `title`, `added_at`, `added_by`, soft delete.
|
||||
- `qc_docs`: `project_id`, `unit_id`, `type`, `rev`, `asset_id`, `auto`, `superseded_by` (the newer row), `added_at`, `added_by`.
|
||||
- `itps`: one per unit (`unit_id` unique). `template` jsonb, frozen when the ITP is made. `status` (itp_status), `signoff_*` (with the signature and the signed PDF asset), `reopen_*`, `version`.
|
||||
- `itp_lines`: `key` (unique per ITP), `result`, `by`, `at`, `ncr`, `note`, `signature_id`, `row_version`.
|
||||
- `itp_rejects`: append-only. `line_key`, `ncr` (required), `note`, `at`, `by`. Every reject keeps its NCR id, even after the line is approved later.
|
||||
- `itp_blocks`: `block_key` (unique per ITP), `signed_at`, `signed_by`, `signature_id`.
|
||||
|
||||
### Packet documents
|
||||
|
||||
`packet_docs`: `asset_id`, `kind` (the closed set below), `title` (the kind label when none is given), `note`, `added_at`, `added_by`, and `removed_at`, `removed_by`, `removed_reason`. The asset must be a PDF, PNG or JPEG.
|
||||
|
||||
| kind | Label |
|
||||
|---|---|
|
||||
| nde | NDE report (RT / UT / MT / PT) |
|
||||
| hydro | Hydrotest report |
|
||||
| dimensional | Dimensional report |
|
||||
| pmi | PMI report |
|
||||
| coating | Coating / paint report |
|
||||
| coc | Certificate of conformance |
|
||||
| customer | Customer inspection report |
|
||||
| ncr | NCR |
|
||||
| other | Other |
|
||||
|
||||
Taking a document out of the data book sets the three `removed_*` columns, with a reason. The row stays. The packet read lists it with `removed` set, so the record shows what was taken out, by whom and why. A row already taken out cannot be taken out again (409 `already_removed`).
|
||||
|
||||
### Q.C. folder documents
|
||||
|
||||
`qc_docs`: one row per filed document, on a project (`project_id`) or a unit (`unit_id`), never both. `type` (the label), `title`, `rev`, `note`, `asset_id` (PDF, PNG or JPEG), `auto` (filed by the ITP sign-off, titled with the ITP number and "signed off" as the old build, rev null), `added_at`, `added_by`, and the supersession columns `superseded_by`, `superseded_at`, `superseded_by_user`.
|
||||
|
||||
A new revision is a new row. In the same transaction, under `SELECT ... FOR UPDATE` of the old row, the old row gets `superseded_by` (the new row's id), `superseded_at` and `superseded_by_user`. The old row is never changed otherwise and never deleted. A row is current while `superseded_by` is null. The revision must be in the same folder as the row it replaces (422 `invalid_replace`); an old row that already points at a newer one is 409 `already_superseded`.
|
||||
|
||||
A unit folder shows its own current rows and inherits the project's current rows. A packet's read lists both as `qc_folder_docs`.
|
||||
|
||||
### The ITP
|
||||
|
||||
- `itps`: one per unit (`unit_id` unique). `template` jsonb is a copy of the code template `ITP_Generation_Revised_2`, frozen when the ITP is started, so a later template change never moves an existing ITP. `no`, `hdr`, `status` (`open` or `complete`), `signoff_at`, `signoff_by`, `signoff_signature_id`, `signoff_asset_id` (the signed PDF, set only when the phase 7 hook files one), `reopen_at`, `reopen_by`, `reopen_reason`, `created_at`, `created_by`, `version` (bumped by every ITP write).
|
||||
- `itp_lines`: one row per template item, all made when the ITP is started, so every line has a `row_version` from the first read. `key` is the item number (`1.3`, unique per ITP). `result` is null (open), `approve`, `reject` or `na` (N/A is stored as the result). `by`, `at` and `initials` are the approval; removing it clears those and `result` and keeps `ncr` and the reject history. `ncr` is the NCR of the latest reject, `note` the line note. `cust_code`, `cust_by`, `cust_at`, `cust_rec`, `cust_rec_at` are the customer entry. `row_version` is for the If-Match on approval. A line is done when it is N/A or approved.
|
||||
- `itp_rejects`: append-only. One `reject` row per reject (`ncr` may be null), and one `ncr` row when the number is entered later. `line_key`, `ncr`, `note`, `kind`, `at`, `by`. The rows survive the later approval of the line.
|
||||
- `itp_blocks`: one row per signature block, all made when the ITP is started (`block_key` is the number of the section that closes the block, `3` to `9`; unique per ITP). An unsigned row holds `qn`, `remarks` and the customer complete fields. `signed_at`, `signed_by`, `signature_id` are the PESCO Q.C. signature. Removing it clears those three and keeps the rest.
|
||||
- `itp_log`: append-only. One row per ITP write: `by`, `by_name`, `action`, `note`, `signature_id` (block sign and sign-off), `it_reason` (an admin edit). See [POLICY.md](POLICY.md), "Log texts".
|
||||
|
||||
## Append-only set
|
||||
|
||||
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`, `co_verifications`.
|
||||
`audit_events`, `signin_attempts`, `signatures`, `project_renumbers`, `short_receipts`, `packet_transitions`, `qc_history`, `packet_log`, `itp_rejects`, `itp_log` (0007), `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`.
|
||||
|
||||
## Audit, settings, jobs (phases 1 and 3)
|
||||
|
||||
- `audit_events` (migration 0001): append-only. `at`, `user_id`, `session_id`, `role_snapshot` (`system` for the worker), `action`, `subject_type`, `subject_id`, `packet_id`, `detail` jsonb, `it_reason`, `ip`. A hash chain: `row_hash` is sha256 over `prev_hash` and the canonical JSON of the row, and `prev_hash` is the row before it. BRIN index on `at`.
|
||||
- `settings` (migration 0001): `key` (text PK), `value` jsonb, `version`, `updated_at`, `updated_by`. Phase 3 adds the row `feed`, the feed settings as one JSON object (see [FEED.md](FEED.md), "Settings"). Phase 5 adds the row `checklist_templates` (see "Checklist templates"). A missing key takes its default. A save bumps `version` and sends `ps_notify('ps_settings', ...)` so the worker reschedules.
|
||||
- `settings` (migration 0001): `key` (text PK), `value` jsonb, `version`, `updated_at`, `updated_by`. Phase 3 adds the row `feed`, the feed settings as one JSON object (see [FEED.md](FEED.md), "Settings"). Phase 5 adds the row `checklist_templates` (see "Checklist templates"). Phase 6 adds the row `qc_check_items` (see "Q.C. check items"). A missing key takes its default. A save bumps `version` and sends `ps_notify('ps_settings', ...)` so the worker reschedules.
|
||||
|
||||
Phase 3, migration 0004:
|
||||
|
||||
@@ -273,7 +347,7 @@ Column additions in 0004:
|
||||
## Versioning
|
||||
|
||||
- `packets.version` goes up by one in every write transaction that touches the packet or any of its children. The write pipeline does it explicitly (`UPDATE packets SET version = version + 1 ... RETURNING version`) after the caller's SQL. No trigger bumps it, so a read never does. It is the packet ETag and the NOTIFY payload.
|
||||
- `projects.version`, `units.version` and `itps.version` play the same part for those rows.
|
||||
- `projects.version`, `units.version` and `itps.version` play the same part for those rows. An ITP write locks the `itps` row (`SELECT ... FOR UPDATE`), not a packet row, and bumps `itps.version`. A Q.C. folder revision locks the row it replaces.
|
||||
- Child rows that are edited in place carry `row_version`. A PATCH sends `If-Match: <row_version>`. A mismatch is 409 `row_conflict` with the current row. The write bumps `row_version`.
|
||||
- Every write takes `SELECT ... FOR UPDATE` on the packet row first. That lock replaces the old 8-second lease and serialises the counters: `next_weld_no`, `next_heat_no`, `next_page_no` and `next_co_no` hand out the next number with `UPDATE packets SET next_weld_no = next_weld_no + 1 RETURNING next_weld_no - 1`, so two concurrent adds never get the same number.
|
||||
- Transitions do not check `version`. They compare and set on `expect: {status, dept}`, so a weld edit never blocks a send.
|
||||
|
||||
+13
-1
@@ -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 5. 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). Every other control names the phase that builds it.
|
||||
Status: phase 6. 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. Every other control names the phase that builds it.
|
||||
|
||||
Contents:
|
||||
|
||||
@@ -109,6 +109,18 @@ The M: drive build had 20 layers of protection, most of them enforced in the bro
|
||||
- A borrowed heat map is read-only. Nothing on the sibling packet can be written through the borrowing packet.
|
||||
- Presence over SSE carries display names, never other personal data. Each entry is the user id, display name, screen and packet id.
|
||||
|
||||
### Q.C. rules (phase 6)
|
||||
|
||||
- Accept is refused on the server until the Q.C. record is complete (`qc_missing_empty`: every check item, a visual result on every weld and none rejected, an acceptable NDE result when a method is set, the inspector name). The screen only shows the reason.
|
||||
- A block signature is refused until every line of the block is initialed or N/A. The sign-off is refused until every block is signed. Both are server rules. A signed-off ITP refuses every write but the reopen, which needs a Q.C. Supervisor, or an admin with an IT reason, and a reason.
|
||||
- An ITP line rejected without an NCR number cannot be approved (server check). Line initials and block signatures can be removed only by the person who made them, or an admin.
|
||||
- NCR numbers are checked against the pattern `NCR-YYYY-NNNN` on the server. Checking them against pesco-ncr is reserved for a later phase (`domain/ncr.py`, `validate_ncr`). The NCR link opens in a new tab with `rel="noopener noreferrer"`.
|
||||
- Documents are never deleted. A packet document is taken out of the data book with a reason and the row is kept. A Q.C. folder document is replaced by a new revision; the old row stays and points at the new one. Packet document kinds and Q.C. document types are closed sets, checked on the server and by database CHECKs. Every document file is a PDF, PNG or JPEG, typed by content.
|
||||
- `itp_log` is append-only, like `packet_log`: the app role has SELECT and INSERT only, and a trigger refuses updates, deletes and truncate. `itp_rejects` stays append-only, so every reject and its NCR number survive a later approval.
|
||||
- Signatures verified on the server, with a content hash: Q.C. accept, Q.C. accept and route, Q.C. reject, the ITP block signature and the ITP sign-off.
|
||||
- The ITP and the Q.C. folder are checked with `check_scoped()`: the same system, admin bypass, who and precondition steps as a packet write. An admin outside Q.C. and Clerical needs an IT reason, recorded in `itp_log` and the audit row.
|
||||
- Every ITP write and every Q.C. folder document writes an audit row (`itp.<action>`, `qc_doc.add`, `qc_doc.revise`).
|
||||
|
||||
### Worker (phase 3)
|
||||
|
||||
- The worker publishes no port and joins no proxy network. It reaches out to VISUAL and M-Files only; nothing reaches in. Admin and Clerical reach it only through rows in PostgreSQL (`job_requests`, the `feed` settings row) and NOTIFY.
|
||||
|
||||
Reference in New Issue
Block a user