Phase 7: close, file, PDFs and prints
ci / frontend (push) Successful in 40s
ci / server (push) Successful in 10m56s
ci / image (push) Successful in 48s
ci / e2e (push) Successful in 2m44s

Clerical close and file: the VISUAL observed state beside the close confirm, a 409 with a second confirm when the
feed still shows work orders open, filing with a signature and the HMAC seal, the data book built on the server
and stored on the seal in the filing transaction, admin reopen keeping the broken seal with its data book.

Server PDFs with reportlab, pypdf and pypdfium2: the manufacturing data book page for page after the old build
(cover, index, drawings with VOID stamps, traceability and heat map rasters, weld maps and welding records, Q.C.
inspections, Q.C. and test documents, MTRs, record history), the job traveler with work order QR codes, the ITP
PDF filed automatically at sign-off, every print kind with the PRINTED COPY stamp, the controlled-copy stamp
signed with the seal key, PDF metadata, the traceability CSV with the formula guard, and the append-only
print_log with an audit row for every print and export (migration 0008).

Frontend: the close and file panel, the print menu and its entry points, the printed copies list on the History
tab, the ITP PDF buttons. Tests: server (close, file, reopen, seal, the builders, prints, CSV, stamps), frontend,
and the Playwright close and print scenarios. Docs updated; SECURITY closes M2, S6, S7 and S8.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-10-17 23:49:14 -06:00
co-authored by Claude Fable 5.1
parent 00531cb329
commit 904437153d
59 changed files with 8226 additions and 136 deletions
+39 -4
View File
@@ -1,6 +1,6 @@
# Packet Station schema
Status: phases 1 to 4 built (identity, organisation, projects, packets, change orders, documents, jobs, VISUAL and drawing caches, the warehouse columns in use, the `notification_events` placeholder). Phase 5 adds the shop columns, the checklist templates and the clock rules (migration 0006). Phase 6 adds the Q.C. columns, the Q.C. check items, the Q.C. folder metadata, the ITP header, line and block details and the ITP log (migration 0007). Phase 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 7 adds the print log and the data book on the seal (migration 0008). 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
@@ -103,7 +103,7 @@ What the warehouse columns mean in practice (phase 4):
- `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`, `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_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. Migration 0008 adds the filed data book (`databook_asset_id`, `databook_pages`) and `open_wos`. See "Close and file (phase 7, migration 0008)".
- `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`).
- `packet_visual_state`: one row per packet, written by the worker. `legs` jsonb, `ticket_hours`, `order_ref`, `observed_at`.
@@ -234,6 +234,41 @@ The texts may hold `{wps}` (else `the WPS`), `{stamp}` (`[75] ` with a trailing
- 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).
## Close and file (phase 7, migration 0008)
### Column additions
- `packet_seals`: `databook_asset_id` (uuid, references `assets`), the data book PDF built and stored when the packet was filed. `databook_pages` (int), its page count. `open_wos` (text[]), the work orders the feed showed open in VISUAL when Clerical filed anyway, as `wo_base/lot` text (`L489052/1`), or null.
### The print log
`print_log` (new, append-only): one row per print or export.
| Column | Meaning |
|---|---|
| `id` | bigserial |
| `at` | when, default `now()` |
| `by`, `by_name` | the user and a snapshot of the name |
| `session_id` | the session that asked |
| `packet_id`, `unit_id`, `itp_id` | what was printed; `unit_id` and `itp_id` for an ITP |
| `kind` | `traveler`, `map-h`, `map-w`, `clean`, `void`, `databook`, `doc`, `itp`, `csv`, `databook-export`, `itp-pdf` |
| `what` | the human label as printed: `Job traveler`, `Drawing with heat map (sheet 2)`, `MTR - item 3` |
| `pages` | the page count |
| `asset_id` | the document printed for kind `doc`; the filed data book for `databook` |
| `stamp` | the stamp line as printed |
| `stamp_sig` | the 16 hex characters signed into the stamp |
| `ip` | the client address |
Indexed on (`packet_id`, `id`) and (`itp_id`, `id`). `GET /api/packets/{id}/prints` reads it newest first. Prints and exports write no `packet_log` row. They are not packet changes.
### The seal and the data book
- FILE writes the `packet_seals` row, then builds the data book in the same transaction, stores it as an `assets` row (`source` `databook`, `source_ref` `{packet_id, seal_id}`) and sets `databook_asset_id` and `databook_pages` on the seal. A data book that cannot be built fails the filing. Nothing is filed half way.
- ADMIN_REOPEN sets `broken_at`, `broken_by` and `broken_reason` on the unbroken seal. The row and its data book stay. They are the record of what was filed.
- Filing again after a reopen inserts a second seal with its own data book. The unique partial index allows one unbroken seal per packet, so the broken rows stay beside it.
- The packet read shows the newest seal row, broken or not, with its data book, and `seals` (how many rows the packet has).
- The signed ITP PDF filed at sign-off is an `assets` row with `source` `itp_signoff`, stored on `itps.signoff_asset_id`.
## 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.
@@ -287,7 +322,7 @@ A unit folder shows its own current rows and inherits the project's current rows
### 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).
- `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 at sign-off since phase 7), `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.
@@ -297,7 +332,7 @@ A unit folder shows its own current rows and inherits the project's current rows
These tables have the `ps_append_only` row and TRUNCATE triggers, and `ps_app` holds only SELECT and INSERT on them:
`audit_events`, `signin_attempts`, `signatures`, `project_renumbers`, `short_receipts`, `packet_transitions`, `qc_history`, `packet_log`, `itp_rejects`, `itp_log` (0007), `co_verifications`.
`audit_events`, `signin_attempts`, `signatures`, `project_renumbers`, `short_receipts`, `packet_transitions`, `qc_history`, `packet_log`, `itp_rejects`, `itp_log` (0007), `print_log` (0008), `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`.