Phase 7: close, file, PDFs and prints
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:
+13
-3
@@ -216,7 +216,7 @@ Conventions: identity bigints except users (uuid = AD objectGUID); Postgres enum
|
||||
Need an answer before the named phase. Defaults apply if none arrives in time.
|
||||
|
||||
1. **Station timer** (Seth, before phase 5): keep, cross-check against labor tickets, or drop. Default: keep the timer, show ticket hours beside it. Phase 5 (2026-10-16) applied the default: the station timer is the clock of record and the VISUAL ticket hours are shown beside it.
|
||||
2. **PDFs on the server** (confirm before phase 7): reportlab + pypdf + pypdfium2. Default: yes.
|
||||
2. **PDFs on the server** (confirm before phase 7): reportlab + pypdf + pypdfium2. Default: yes. Phase 7 (2026-10-17) applied the default: reportlab 5.0.1, pypdf and pypdfium2, with Pillow 13.0.0 for the rasters.
|
||||
3. **Frontend stack** (confirm before phase 1): TypeScript + React + Vite. Default: yes. Plain JavaScript would not be faster for parity: the old 7,000 lines are screen code that must be rewritten either way, and typed API models cut rework.
|
||||
4. **PIN defaults** (confirm): 6 to 12 digits, five attempts, 10-hour shop session, device cookie. Genuinely open (Seth and Q.C., before phase 6): whether Q.C. accept and filing require the domain password even with a PIN set. Default: PIN is enough. IT: device certificate later.
|
||||
5. **Several open orders on a project** (before phase 3): which order a packet shows. Proposed rule: the open order whose line's top job is the packet's work order base, else the newest open order, else the newest closed one; the packet shows "via base" or "via project" and Clerical can override. Needs Seth's view. Phase 3 (2026-10-16) built the default in one function (`feed/orders.py`): a Clerical pin, else the base's own linked order, else the project order whose top job is the unit, else the project's first open order, else its newest; the source (`via`) is stored on the project and unit and shown on the project page, which lists every open order. Seth may change the rule later in that one place.
|
||||
@@ -257,12 +257,22 @@ From phase 6 (2026-10-17), for Seth and Q.C. to veto:
|
||||
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.
|
||||
31. The signed ITP PDF on sign-off: default a hook (`pdf.itp.file_signed_itp_pdf`) that phase 7 builds. Today it files nothing, the sign-off log says `PDF filing reserved for phase 7`, and the automatic `qc_docs` ITP row is written only when the hook returns a file. Applied in phase 6. Phase 7 (2026-10-17) built the hook: the sign-off files the signed ITP PDF and the automatic ITP document in the same transaction, and a PDF that cannot be built fails the sign-off.
|
||||
|
||||
From phase 7 (2026-10-17), for Seth and Clerical to veto:
|
||||
|
||||
32. Prints: the old build pushed page images into the browser's print dialog and offered the same PDF when the dialog was blocked. Default: a print opens as a stamped PDF in a new browser tab, and the person prints it from the PDF viewer (Ctrl+P). One path is simpler and prints the same stamped pages. Phase 7 (2026-10-17) applied the default.
|
||||
33. When the data book is built: default on the server when the packet is filed, inside the filing transaction, stored as an asset on the seal. A data book that cannot be built fails the filing, so nothing is filed half way. Before filing, the data book can be viewed or downloaded at any time from status complete on; that copy is stamped "not yet filed". Phase 7 (2026-10-17) applied the default.
|
||||
34. Who may read the PDFs and the CSV: default every signed-in role that can open a packet, PM included. Every print and every export is logged with who, when, what and how many pages, in the append-only `print_log` and in the audit. Phase 7 (2026-10-17) applied the default.
|
||||
35. The old "Original packet PDF" print kind: the rebuild has no VISUAL packet PDF. The drawing pages come from M-Files or a hand upload, and "Drawing (no markup)" prints them as attached. Default: the kind is gone. Phase 7 (2026-10-17) applied the default.
|
||||
36. Filing while the feed still shows a work order open in VISUAL: default a second confirmation (`confirm_open_wos`), a warning and not a block, on both routes. The work orders are kept on the seal and in the log note. A hand packet has no feed state and no warning. Phase 7 (2026-10-17) applied the default.
|
||||
37. The controlled-copy stamp carries a short signature made with the seal key, so a printed or exported copy can be checked against the server later. Default: phase 7 ships the signature, the check function and the print log; phase 9 adds the Admin screen that checks a copy. Phase 7 (2026-10-17) applied the default.
|
||||
|
||||
## 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.
|
||||
- Phase 3: a recorded DEV feed run compared with the packets the old build made for the same window (counts of drafts, units, projects; same keys), run from the developer machine against VISUAL DEV only.
|
||||
- Phase 7: the data book of a synthetic filed packet compared page by page with a data book exported from the old build on the same fixture.
|
||||
- Phase 7: server tests for close and file against the policy oracle, the open work order confirm, the seal with its data book, reopen and refile, a failed data book build, the data book sections and index, the traveler, the ITP PDF, every print kind with its stamp and `print_log` row, the CSV formula guard and the stamp signature; frontend tests for the close and file panel, the print menu, the prints list and the ITP buttons; the Playwright close scenario (close, PDFs and CSV, prints, file with the seal, reopen and refile, the signed ITP PDF) in the CI stack on docker-1.
|
||||
- Phase 8: the data book of a synthetic filed packet compared page by page with a data book exported from the old build on the same fixture (moved from phase 7).
|
||||
- Phase 9: restore rehearsal on the test stack from a pg_dump and an assets copy.
|
||||
- Phase 10: the full 38-scenario bench green in Gitea Actions.
|
||||
|
||||
+62
-3
@@ -211,7 +211,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 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, 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
|
||||
|
||||
@@ -556,7 +556,7 @@ The itp_edit_line writes:
|
||||
- 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.
|
||||
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 `pdf.itp.file_signed_itp_pdf()`, which builds the signed ITP PDF (phase 7). 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 PDF that cannot be built answers 500 `itp_pdf_failed` and nothing is signed off. 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).
|
||||
|
||||
@@ -618,11 +618,70 @@ The `action` and `note` of `itp_log` (and the audit row's `detail.text`) for eac
|
||||
| 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` |
|
||||
| Sign-off | `ITP signed off` | `PDF filed` |
|
||||
| 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`.
|
||||
|
||||
## 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.
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
### 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.").
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### The filed lock
|
||||
|
||||
A filed packet refuses every write except SET_DATABOOK_AT and ADMIN_REOPEN, Admin included (code `filed`). Reads, PDFs, the CSV and prints stay open.
|
||||
|
||||
### The seal and reopen
|
||||
|
||||
The seal is HMAC-SHA256 with the server seal key over the packet's read model in seal mode. `GET /api/packets/{id}/seal` rebuilds the content and answers `{seal, valid, seals}`. `valid` is null when the key is not available.
|
||||
|
||||
ADMIN_REOPEN is Admin, from filed, with a reason of at least 8 characters and a signature ("Reopen a filed packet"). The packet goes back to complete. The seal is marked broken with `broken_at`, `broken_by` and `broken_reason`. The row and its data book are kept: they are the record of what was filed. Filing again writes a second seal with its own data book. The packet read shows the newest seal, broken or not, with its data book, and `seals`, the number of seal rows.
|
||||
|
||||
### PDFs, the CSV and prints
|
||||
|
||||
Every read below needs a signed-in session and a packet that exists and is not removed (404 `not_found`). Every role that can open the packet may read them, PM included. No policy action guards them, and the filed lock does not apply. Each one writes a `print_log` row and an audit row after the bytes are built.
|
||||
|
||||
| Endpoint | What | When | Log kind | Audit |
|
||||
|---|---|---|---|---|
|
||||
| `GET /api/packets/{id}/pdf/databook` | The filed data book as stored, or one built now and stamped "not yet filed" | complete or filed, else 409 `wrong_status` "The data book is built once the packet is back from Q.C." | `databook-export` | `export` |
|
||||
| `GET /api/packets/{id}/pdf/traveler` | The job traveler with the work order QR codes | any status | `traveler` | `export` |
|
||||
| `GET /api/packets/{id}/export/traceability.csv` | The traceability CSV, with the formula guard | any status | `csv` | `export` |
|
||||
| `GET /api/packets/{id}/print/{kind}` | A printed copy: `traveler`, `map-h`, `map-w`, `clean`, `void`, `databook`, `doc` | `databook` from complete on (409); `void` only with void pages (404 `nothing_to_print` "Nothing to print."); `doc` only for an asset of the packet, its unit or project folder, or its filed data book (404) | the kind | `print` |
|
||||
| `GET /api/packets/{id}/prints` | The print log of the packet, newest first, 200 rows | any status | | |
|
||||
| `GET /api/itps/{id}/pdf` | The ITP PDF built now, stamped signed off or open | any status | `itp-pdf` | `export` |
|
||||
| `GET /api/itps/{id}/print` | The ITP PDF stamped PRINTED COPY | any status | `itp` | `print` |
|
||||
|
||||
`?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`.
|
||||
|
||||
### 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, 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, the data book | `Data book filed` | `N pages` |
|
||||
| admin_reopen | `Reopened by IT` | the reason |
|
||||
| set_databook_at | `Data book date set` | |
|
||||
|
||||
The audit gains `print` (detail `{kind, what, pages, stamp_sig}`, plus `sheet` and `asset_id` when given) and `export` (detail `{kind, what, pages, stamp_sig}`, kind `databook`, `traveler`, `csv` or `itp`). FILE, ADMIN_REOPEN and SET_VCLOSED write their audit rows through the write pipeline as before.
|
||||
|
||||
## 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.
|
||||
|
||||
+39
-4
@@ -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`.
|
||||
|
||||
|
||||
+27
-9
@@ -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 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.
|
||||
Status: phase 7. The identity controls (sessions, PIN, rate limits, audit, Origin check, derived group map) and the policy controls (server policy on every write, workflow steps as compare-and-set, server-side validation and limits, IT edit reasons, the filed lock, the upload pipeline for drawings and documents) are built and tested. Phase 3 adds the worker controls (read-only VISUAL and M-Files reads, stack guard, advisory locks, redacted errors, audited system writes). No requirement ID closes in phase 3. Phase 4 completes the upload pipeline (a kind per upload with its own types and limit, XLSX and CSV by content, the optional ClamAV scan, the assets volume quota alert) and adds the warehouse rules (the heat gate on release, closed hold and short reasons, receive by custody). Phase 4 closes D5. Phase 5 adds the shop rules (the server as the clock of record, server-assigned mark numbers under the packet row lock, insert-only change-order verifications with a signature, VOID sheets never edited). Phase 6 adds the Q.C. rules (server gates on accept, block sign and sign-off, NCR numbers checked by pattern, Q.C. documents never deleted, the append-only ITP log, server-verified signatures on every Q.C. and ITP sign-off). No requirement ID closes in phase 6. Phase 7 adds the close and file controls (the HMAC seal with the data book filed on it, the controlled-copy and print stamps signed with the seal key, `print_log` and audit rows for every print and export, the CSV formula guard, PDF metadata, escaping in the PDFs). Phase 7 closes M2, S6, S7 and S8. Every other control names the phase that builds it.
|
||||
|
||||
Contents:
|
||||
|
||||
@@ -21,10 +21,10 @@ The M: drive build had 20 layers of protection, most of them enforced in the bro
|
||||
| 3 | Employee ID + PIN sign-in | Replaced by AD sign-in with the domain password, plus an optional Packet Station PIN checked on the server, for registered shop devices only. | 1 |
|
||||
| 4 | Department and level screens | A static role-to-screen map in the browser, plus server policy on every request. The browser only decides what to show. | 1, 2 |
|
||||
| 5 | Per-PC access log | Replaced by the server audit. Each row carries the AD account, session, client IP and user agent. No per-PC browser ids. | 1 |
|
||||
| 6 | Filing seals and filed-packet lock | HMAC seals computed on the server with a server key. The filed lock is a server policy rule. | 7 |
|
||||
| 6 | Filing seals and filed-packet lock | HMAC seals computed on the server with a server key. The filed lock is a server policy rule. Built. | 2, 7 |
|
||||
| 7 | Record integrity check | A worker job (seals, asset hashes, audit hash chain) with an alert. | 9 |
|
||||
| 8 | Self-hosted libraries | Kept: no CDN. The CSP allows nothing external. | 0 |
|
||||
| 9 | Output escaping | React escapes output by default. The PDF export escapes on the server. | 1, 7 |
|
||||
| 9 | Output escaping | React escapes output by default. The PDF builders clean and escape every text on the server. Built. | 1, 7 |
|
||||
| 10 | Central permission policy | Server policy, checked on every write, including writes that do not come from the screens. Built. | 2 |
|
||||
| 11 | PIN sign-off (electronic signature) | Server-verified signature with the PIN or the domain password, stored with who, when, what, factor and a content hash. | 1 |
|
||||
| 12 | IT edit reasons | Kept. Admin writes need a reason and are marked as IT edits in the packet history and the audit. | 2 |
|
||||
@@ -32,8 +32,8 @@ The M: drive build had 20 layers of protection, most of them enforced in the bro
|
||||
| 14 | Input and upload validation | On the server: field limits on every write, closed reason sets, uploads typed by content per kind (drawing PDF; MTR and document PDF, PNG or JPEG; BOM XLSX or CSV), a size limit per kind (25 MB, BOM 5 MB), sanitised names, an optional ClamAV scan before anything is stored, and a quota alert on the assets volume. Built. | 2, 4 |
|
||||
| 15 | Second-admin approval | Superseded. Roles come only from AD groups and the group-to-role map is derived, so there is nothing to grant in the app. | 1 |
|
||||
| 16 | Security alerts and automatic integrity check | Worker jobs and the Notifications tab (email through app-03). | 9 |
|
||||
| 17 | Controlled-copy stamp on data books | Applied to server-built PDFs and signed with the seal key. | 7 |
|
||||
| 18 | CSV formula guard | Applied to every server export. | 7 |
|
||||
| 17 | Controlled-copy stamp on data books | Applied to server-built PDFs and signed with the seal key. Built, phase 7. | 7 |
|
||||
| 18 | CSV formula guard | Applied to every server export. Built, phase 7. | 7 |
|
||||
| 19 | Update lock and stale-version block | Replaced by a build version endpoint. A screen on an older build shows a banner and reloads. | 0 (endpoint), 10 (banner) |
|
||||
| 20 | VISUAL helper write guard | Dropped. There is no helper. VisualKit and MFilesKit are read-only by design and connect as read-only accounts. | 3 |
|
||||
|
||||
@@ -146,9 +146,25 @@ The M: drive build had 20 layers of protection, most of them enforced in the bro
|
||||
|
||||
### Seals (phase 7)
|
||||
|
||||
- Filing writes an HMAC seal with the server seal key. A reopen by Admin needs a reason and keeps the old seal in history.
|
||||
- Filing writes an HMAC-SHA256 seal over the packet's read model in seal mode, with the server seal key (`SEAL_KEY_FILE`, a Docker secret) and its `key_id`. Nobody without the key can recompute it (M2). `GET /api/packets/{id}/seal` rebuilds the content and says whether the seal still matches.
|
||||
- The data book is built on the server inside the filing transaction and stored as an asset on the seal. A data book that cannot be built fails the filing; nothing is filed half way.
|
||||
- Filing while the feed shows a work order open in VISUAL needs a second confirmation. The work orders are kept on the seal and in the packet log.
|
||||
- A reopen by Admin needs a reason and a signature. It marks the seal broken with who, when and why, and keeps the row and its data book. Filing again writes a new seal beside it.
|
||||
- The integrity job checks seals and asset hashes (phase 9).
|
||||
|
||||
### PDFs, prints and exports (phase 7)
|
||||
|
||||
- Every PDF and the CSV are built on the server. The browser only opens the URL. Reads need a signed-in session; every role that can open the packet may read them, PM included.
|
||||
- Controlled-copy stamp (S7): every exported page carries `Controlled copy - exported by Name (emp) <UTC time>`, the seal's first 12 hex or `not yet filed`, and ` - stamp <sig>`. The filed data book is the stored copy, served as it was built.
|
||||
- Printed-copy stamp (S8): every printed page carries `PRINTED COPY - <what> - printed by Name (emp) <UTC time> - <project> Lot <unit> dwg <drawing> R<rev> - verify revision before use` and the stamp signature, in a white box at the bottom of the page.
|
||||
- Stamp signature: 16 hex of HMAC-SHA256 with the seal key over the packet, the kind (a printed copy signs `print:<kind>`, so a print and an export of the same thing in the same minute never share a signature), the seal hash, the user and the time. A copy can be checked against the server later with `verify_stamp`; the Admin screen for it comes in phase 9.
|
||||
- Logging (S7, S8): every print and export writes one `print_log` row (who, when, session, packet, unit or ITP, kind, what, pages, the stamp line, the stamp signature, the client address) and one audit row (`print` or `export`). `print_log` is append-only: the app role has SELECT and INSERT only, and a trigger refuses updates, deletes and truncate.
|
||||
- CSV formula guard (S6): a cell that starts with `=`, `+`, `-`, `@`, a tab or a CR gets a leading `'`. A cell with a quote, a comma or a line break is quoted with the quotes doubled.
|
||||
- PDF metadata: title, subject and keywords name the packet and the stamp signature; Author is PESCO and Producer is Packet Station.
|
||||
- Output escaping (S5): every text drawn into a PDF goes through `clean` (control characters removed, characters outside Latin-1 replaced) and is XML-escaped where a Paragraph renders it, so data from VISUAL, M-Files or a person cannot inject markup.
|
||||
- Responses carry `Content-Disposition` (inline, or attachment with `?download=1`), `X-Content-Type-Options: nosniff` and `Cache-Control: no-store`. A print of a document checks that the asset belongs to the packet, its unit or project folder, or its filed data book; anything else is 404.
|
||||
- The signed ITP PDF is built and filed in the sign-off transaction. A PDF that cannot be built fails the sign-off.
|
||||
|
||||
### Headers (phase 0)
|
||||
|
||||
- The app sets HSTS, CSP and the other security headers itself. NPM's own HSTS is off.
|
||||
@@ -185,15 +201,17 @@ The IDs come from Part 3 of the old SECURITY.md. The status follows the approved
|
||||
| D5 | Closed, phase 4 (optional ClamAV: a `clamav` compose profile; when `CLAMAV_HOST` is set every upload and every stored drawing is scanned before it is kept, a hit is refused and audited, and an unreachable scanner refuses uploads; off when the host is not set) |
|
||||
| D6, D7 | Deferred (retention period and customer copy need a decision from Seth and Q.C.) |
|
||||
| M1 | Closed, phase 2 (append-only audit and packet log written only by the server; every packet write carries both) |
|
||||
| M2 | Closed, phase 7 |
|
||||
| M2 | Closed, phase 7 (filing seal is HMAC-SHA256 with the server seal key from a Docker secret; a reopen keeps the broken seal and its data book) |
|
||||
| M3 | Closed, phase 9 |
|
||||
| M4, M5 | Deferred (logs to stdout now; SIEM shipping is IT's) |
|
||||
| S1 | Closed, phase 0, tuned phase 10 |
|
||||
| S2 | Closed, phase 1 |
|
||||
| S3 | Closed, phase 9 (CI scanning) |
|
||||
| S4 | Deferred to IT (pentest before go-live) |
|
||||
| S5 | Closed, phase 2 (React escapes output; server messages are plain text; PDF export escaping comes with phase 7) |
|
||||
| S6, S7, S8 | Closed, phase 7. PLAN.md lists S8 under phase 5, but the print stamps come with the PDFs in phase 7, so S8 closes there. |
|
||||
| S5 | Closed, phase 2 (React escapes output; server messages are plain text); phase 7 adds escaping in the PDF builders |
|
||||
| S6 | Closed, phase 7 (the CSV formula guard on every export) |
|
||||
| S7 | Closed, phase 7 (the controlled-copy stamp on every exported PDF, signed with the seal key; PDF metadata; every export logged in `print_log` and the audit) |
|
||||
| S8 | Closed, phase 7 (the PRINTED COPY stamp with "verify revision before use" on every printed page, signed with the seal key; every print logged in `print_log` and the audit). PLAN.md lists S8 under phase 5, but the print stamps come with the PDFs in phase 7, so S8 closes there. |
|
||||
| O1, O2 | Closed, phase 9 (pg_dump timer plus PBS; one rehearsed restore); quarterly restore IT's |
|
||||
| O3, O4 | Closed, phase 10 |
|
||||
| O5 | Superseded (no migration; M: retired read-only) |
|
||||
|
||||
Reference in New Issue
Block a user