Phase 5: Supervisor and Stall screens, weld map, heat table, clock and CO verification
The shop at parity with the old build. Supervisor screen (department chips, CO headline, numbers, Change orders
on hold, station grid with clocks and CO tags, boards) and Stall screen (station picker, hero, My packets,
Submitted). Shop steps on the packet page: assign with station, welder picker and stamp; pull back; Start, Pause,
Resume; Submit with a PIN signature and the missing steps as links; review with Send to Q.C., Return to station
and Skip Q.C. route; a lead's read-only review. Weld map (pdf.js viewer with pan, pinch, zoom, full screen; marks
with leader lines; weld sheet; weld record with field CAS; carried marks). Heat map (heat table placed, dragged,
sized, removed; legacy amber tags; borrowed heat map read-only). Checklist tab with templates from settings.
VOID stamps on voided pages. Change orders: CO-n labels, supervisor verify with signature, approval copies the
heat table to the new sheet and the BOM merge keeps the shop's values. Server clock of record with VISUAL ticket
hours beside it. Presence per packet over SSE. Migration 0006 (bfx, bfy, carried_from, checklist_templates).
New endpoints /api/shop/board, /api/shop/stations, /api/shop/stall, /api/departments/{code}/people.
Tests: policy oracle for every shop action, role and status; clock arithmetic; concurrent inserts; CO apply;
frontend geometry, heat table, clock, checklist, weld sheet, screens; seven Playwright scenarios. Docs updated.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
+13
-1
@@ -414,11 +414,23 @@ For CI and local runs the worker can read files instead of VISUAL and M-Files:
|
||||
|
||||
Real job data never enters git or CI. Recordings from VISUAL DEV stay local, in `tests/fixtures/local/` and `recordings/`.
|
||||
|
||||
## Ticket hours beside the station clock
|
||||
|
||||
The packet read model has a `labor` field from `visual_labor`. It joins the packet's work orders on (`wo_base`, `lot`, `split`, `sub`, `seq`) and sums per work order:
|
||||
|
||||
- `hours` and `tickets`: the totals over the packet's work orders.
|
||||
- `per_wo`: one entry per work order leg, with `wo_base`, `lot`, `split`, `sub`, `seq`, `hours`, `tickets` and `last_ticket`.
|
||||
- `observed_at`: when the worker last read the tickets.
|
||||
|
||||
`labor` is null for a hand packet, or when no `visual_labor` row matches. The packet page shows the sum beside the station clock, as `VISUAL tickets 3.2 h`.
|
||||
|
||||
The station timer stays the clock of record (open question 1, default kept). The ticket hours are shown for comparison only. Nothing is computed from them, and they never change the clock. The screen reads them from PostgreSQL, never from VISUAL.
|
||||
|
||||
## What the screens read
|
||||
|
||||
Screens read PostgreSQL only, never VISUAL or M-Files per request:
|
||||
|
||||
- Packets, flags, work order status, closed and cancelled state, ticket hours and the chosen order come from the packet tables and `packet_visual_state`.
|
||||
- Packets, flags, work order status, closed and cancelled state, ticket hours and the chosen order come from the packet tables and `packet_visual_state`. The ticket hours beside the station clock come from `visual_labor` (see "Ticket hours beside the station clock").
|
||||
- The From VISUAL panel reads `/api/feed/overview`: pause state, next run, the heartbeat, the last ten feed and drawing runs, the ignored work orders, unmapped resources and projects without names.
|
||||
- Admin reads the cache tables (`/api/feed/batches`, `/api/feed/changes`) for "what VISUAL changed".
|
||||
- The project page reads the cached orders from `visual_order_links` and `visual_orders`.
|
||||
|
||||
+7
-1
@@ -215,7 +215,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.
|
||||
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.
|
||||
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.
|
||||
@@ -245,6 +245,12 @@ From phase 4 (2026-10-16), for Seth to veto:
|
||||
22. Upload kinds and limits: default an MTR is PDF, PNG or JPG up to 25 MB, a hand BOM is XLSX or CSV up to 5 MB. ClamAV is optional and off unless CLAMAV_HOST is set; when set, an unreachable scanner refuses uploads (fail closed).
|
||||
23. The quota alert: default a `notification_events` row (phase 9 builds the delivery on this table) at 85 percent of the assets volume.
|
||||
|
||||
From phase 5 (2026-10-16), for Seth to veto:
|
||||
|
||||
24. Carried marks: when a packet is routed to another department, the welds, heat tags and heat table stay on the packet. The read model computes a `carried` flag. Nothing is copied. Default: computed, not copied.
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
+142
-2
@@ -147,7 +147,7 @@ Each precondition is a named predicate over the packet state and the request inp
|
||||
| short_open | at least one BOM line has an open short (the old whCanReceive) | No part is short on this packet. |
|
||||
| welder_set (input) | the request gives station_id and welder_id | Pick a station and a welder. |
|
||||
| checklist_complete | every key of the department checklist is done | Finish the checklist first. |
|
||||
| welds_recorded | at least one weld, in a weld department | Record at least one weld first. |
|
||||
| welds_recorded | at least one weld in the packet's current department, in a weld department | Record at least one weld first. |
|
||||
| filler_set | filler metal entered, in a weld department | Enter the filler metal first. |
|
||||
| qc_book_spool_location (input) | book and spool confirmed and a location given | Confirm the book and the spool, and give the location. |
|
||||
| qc_missing_empty | the Q.C. record has nothing missing | Q.C. still needs: (the list). |
|
||||
@@ -297,6 +297,142 @@ The `action` column of `packet_log` for each warehouse write. `{item}` is the BO
|
||||
|
||||
When a Pulled tick starts pulling by itself, there is no separate stage row in the log: the line's own row gets ` · started pulling` or ` · pulling resumed` on its note. Every write above also writes its audit row through the write pipeline, with `detail.item`, `wh_from` and `wh_to` as before, and `detail.qty` on short writes.
|
||||
|
||||
## Shop (phase 5)
|
||||
|
||||
### Who takes each step
|
||||
|
||||
| Action | Who | From | Notes |
|
||||
|---|---|---|---|
|
||||
| assign | Lead or supervisor of the packet's department (DEPT_LEAD) | ready | Station and welder checked at assign time (below) |
|
||||
| unassign | DEPT_LEAD | assigned | Clears station and welder, folds the clock |
|
||||
| pull_back | DEPT_LEAD | in_progress | Clears station and welder, folds the clock |
|
||||
| start | Lead or supervisor of the department, or the assigned welder within their station limits (WORKS_ON) | assigned | Starts the clock |
|
||||
| submit | WORKS_ON | assigned, in_progress | Stops the clock. Signature "Submit work to the supervisor" |
|
||||
| return_to_station | Supervisor of the department (DEPT_SUP) | review | Reason |
|
||||
| send_to_qc | DEPT_SUP | review | Signature "Send to Q.C.". Opens the inspection |
|
||||
| route_from_review | DEPT_SUP | review | Signature "Route to another department". Closes the stage and opens one in the target department |
|
||||
| send_back_to_warehouse | DEPT_LEAD | ready | Reason |
|
||||
| return_to_clerical | DEPT_LEAD | ready, only when wh_skipped | Reason |
|
||||
|
||||
A worker who is not the assigned welder cannot start or submit. Neither can the assigned welder when their station limits leave out the packet's station. Every step sends `expect: {status, dept}`, so a stale screen gets 409 `stale_step` "Someone else already took this step."
|
||||
|
||||
### Assign checks
|
||||
|
||||
ASSIGN checks the station and the welder on the server, at assign time:
|
||||
|
||||
| Check | Refusal |
|
||||
|---|---|
|
||||
| The station is an active station of the packet's department | 422 `invalid_station` |
|
||||
| The welder is an active user with a shop role (worker, lead or supervisor) in the packet's department | 422 `invalid_welder` "Pick a welder in this department." |
|
||||
| A worker with station limits (no lead or supervisor role in the department) is limited to the chosen station | 422 `station_limit` "{name} is limited to {station names}." |
|
||||
| A `stamp` in the body matches the stamp pattern | 422 `invalid_stamp` |
|
||||
|
||||
The stamp in the body overrides the user's `welder_stamp` for this assignment. Without it, the user's stamp is used. The packet and the stage row both get the station, the welder and the stamp. `GET /api/departments/{code}/people` gives the assign sheet its welder list, with each person's level and station limits.
|
||||
|
||||
### The lead's review
|
||||
|
||||
A lead holds a packet in review (see "Custody"), but return_to_station, send_to_qc and route_from_review are the supervisor's. The server refuses them to a lead with 403 `forbidden`. The screen shows the lead the review read-only: "A supervisor for 03 Weld Parts reviews it and sends it on."
|
||||
|
||||
### Preconditions on the shop steps
|
||||
|
||||
| Step | Precondition | Passes when | Message |
|
||||
|---|---|---|---|
|
||||
| assign | welder_set (input) | the request gives station_id and welder_id | Pick a station and a welder. |
|
||||
| submit | checklist_complete | every key of the department's checklist is done | Finish the checklist first. |
|
||||
| submit | welds_recorded | at least one live weld whose `dept_code` is the packet's current department, in a weld department | Record at least one weld first. |
|
||||
| submit | filler_set | filler metal entered on the open stage, in a weld department | Enter the filler metal first. |
|
||||
| send_to_qc | qc_book_spool_location (input) | book and spool confirmed and a location given | Confirm the book and the spool, and give the location. |
|
||||
| return_to_station, send_back_to_warehouse, return_to_clerical | reason_given (input) | a reason of at least 8 characters after cleaning | Give a reason of at least 8 characters. |
|
||||
| route_from_review | target_dept_given (input) | a shop department different from the packet's | Choose a department. / Choose a different department. |
|
||||
|
||||
welds_recorded counts only the current department's welds. After a route, the new department needs a weld of its own before it can submit. The welds from earlier departments are carried marks (see [SCHEMA.md](SCHEMA.md), "Carried marks").
|
||||
|
||||
### Change-order hold
|
||||
|
||||
An open change order holds these steps: WH_RELEASE, SUBMIT, SEND_TO_QC, ROUTE_FROM_REVIEW, QC_ACCEPT, QC_ACCEPT_AND_ROUTE and FILE. It does not hold ASSIGN, START, the clock or the edits, so the shop keeps working while the departments verify. A change order is open when it is approved with departments still to verify, or pending with hold on (`v_open_cos`).
|
||||
|
||||
The hold text (the `co_hold` refusal and the `blocked` reason):
|
||||
|
||||
| Change order | Text |
|
||||
|---|---|
|
||||
| Pending, with hold | `CO-2: Clerical is reviewing a change from VISUAL. Held until Clerical approves or rejects it.` |
|
||||
| Approved, departments pending | `Change order CO-2 must be verified first by: 03 Weld Parts, 09 Hydro.` |
|
||||
|
||||
With several open change orders, the texts are joined by a space, one sentence each. The id is `CO-<co_no>` everywhere.
|
||||
|
||||
### Change-order verification
|
||||
|
||||
CO_VERIFY is the supervisor of the department named in the request: a shop Supervisor for a shop code, the WH or Q.C. Supervisor for WH or QC. A lead cannot verify (403 `forbidden`). Clerical never verifies. Each verification needs a signature ("Verify a change order for the department") and is an insert into `co_verifications`. A second one for the same department is 409 `already_verified`. A department the change order does not need is 422 `invalid_dept`. A verification cannot be withdrawn.
|
||||
|
||||
The required departments on approval are the departments the change touches, without Clerical and PM. A minor change order needs none.
|
||||
|
||||
### The clock
|
||||
|
||||
CLOCK is the holder's in the shop statuses. The ops are `start`, `pause` and `resume`. An op in the wrong state is 409 `clock_state`. The server keeps the time with the database clock. No request sets the elapsed time. START starts the clock when it is not running. SUBMIT stops it. UNASSIGN and PULL_BACK fold the running time in, so the clock freezes. See [SCHEMA.md](SCHEMA.md), "The clock".
|
||||
|
||||
### Marks on the drawing
|
||||
|
||||
Welds (EDIT_WELDS), heat tags (EDIT_HEATS) and the heat table (EDIT_HEAT_TABLE) follow the rule table: the holder in the shop statuses, and for heat tags and the heat table also the warehouse while it holds the packet. The server hands out `W<n>` and `H<n>` from the packet counters under the packet row lock.
|
||||
|
||||
A VOID sheet refuses marks. A new weld or heat tag on a voided page, or a move of one already there, is 422 `void_page` "This sheet is VOID. Mark the current sheet." The marks on a void page stay and are shown, but cannot be edited.
|
||||
|
||||
### Heat map borrowing
|
||||
|
||||
A packet borrows a sibling's heat map when all of these hold (the old heatSrc):
|
||||
|
||||
- It has no heat map of its own: no heat table placement and no heat tag.
|
||||
- It has no raw material of its own: every BOM line is N/A or a sub-assembly, or the BOM is empty.
|
||||
- It has a drawing.
|
||||
|
||||
The sibling is a live packet in the same unit, with a different id and the same drawing (compared upper-case with white space removed), that has a heat map. The same rev is preferred, then the oldest. The read model gives it as `heat_src`. It is read-only: nothing on it can be written through the borrowing packet. The screen says "Heat map from <op> on this unit (same drawing). Record heats there."
|
||||
|
||||
### Shop screens
|
||||
|
||||
- `GET /api/shop/board` covers the shop departments where the person is lead or supervisor. Admin sees every active shop department. A department outside that scope is 403 `forbidden`. A person who leads no department gets 403 `forbidden` "You do not lead a department." The change order list also shows open change orders anywhere that wait on one of the person's departments.
|
||||
- `GET /api/shop/stations` lists the stations the person may pick for the Stall screen: every station of their shop departments at any level, cut to their station limits for a worker. Admin gets every station.
|
||||
- `GET /api/shop/stall?station_id=` is 403 for a station not in that list. Admin passes.
|
||||
|
||||
### Presence
|
||||
|
||||
The packet page shows who else has the packet open. Presence comes over SSE and carries the user id, display name, screen and packet id only.
|
||||
|
||||
### History texts
|
||||
|
||||
The `action` and `note` columns of `packet_log` for each shop write. A department is shown as code and name (`03 Weld Parts`), except in the verify text, which uses the name only. The station name is as stored (`Stall 3`).
|
||||
|
||||
| Write | action | note |
|
||||
|---|---|---|
|
||||
| assign | `Sent to Stall 3` | `Andy Welder [75]` (the stamp in brackets when set) |
|
||||
| unassign | `Taken off Stall 3` | |
|
||||
| pull_back | `Pulled back from Stall 3` | |
|
||||
| start | `Work started` | `Stall 3` |
|
||||
| submit | `Submitted to supervisor` | `4 welds · 1.5 h` |
|
||||
| return_to_station | `Returned to Stall 3` | the reason |
|
||||
| send_to_qc | `Sent to Q.C.` | `Spool at Bay 2 · note` |
|
||||
| route_from_review | `Routed to 09 Hydro` | `Supervisor, Q.C. skipped · from 03 Weld Parts` |
|
||||
| send_back_to_warehouse | `Sent back to the warehouse` | the reason |
|
||||
| return_to_clerical | `Sent back to Clerical` | the reason |
|
||||
| Weld added | `Weld W4 marked` | `Fillet 5/16" · items 1 + 2 · L662001 #1` |
|
||||
| Weld edited | `Weld W4 changed` | the changed fields, `type Groove · items 1 + 3` |
|
||||
| Weld moved (position only) | `Weld W4 moved` | |
|
||||
| Weld removed | `Weld W4 removed` | the reason |
|
||||
| Heat tag | `Heat tag H2 placed`, `Heat tag H2 changed`, `Heat tag H2 moved`, `Heat tag H2 removed` | `item 3` |
|
||||
| Heat table placed | `Heat table placed on drawing` | the page's dwg, else `sheet 2` |
|
||||
| Heat table moved | `Heat table moved` | the same |
|
||||
| Heat table resized | `Heat table resized` | `A+` or `A-` and the new size, `A+ 1.15` |
|
||||
| Heat table removed | `Heat table removed from drawing` | the same |
|
||||
| Heat table copied on a change order | `Heat table copied to new sheet` | `from the VOID copy, check its position on the new revision` |
|
||||
| Checklist | `Checked: <item text>`, `Unchecked: <item text>` | |
|
||||
| Stage fields | `Weld record changed` | the changed fields, `filler ER70S-6 · filler lot 1234` |
|
||||
| Clock | `Clock started`, `Paused`, `Resumed` | |
|
||||
| Change order issued | `Change order CO-2 issued` | |
|
||||
| Change order approved | `Change order CO-2 approved` | |
|
||||
| Change order rejected | `Change order CO-2 rejected` | the reason |
|
||||
| Change order hold | `Change order CO-2 holds the packet`, `Change order CO-2 no longer holds the packet` | |
|
||||
| Change order verified | `Change order CO-2 verified by Weld Parts` | `note · still waiting on Hydro`, or `note · change order fully verified` |
|
||||
|
||||
The submit hours are the stage's `clock_elapsed_ms` after the stop, with one decimal.
|
||||
|
||||
## 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.
|
||||
@@ -352,7 +488,7 @@ The old policy was looser than the old screens in places. The rebuild follows th
|
||||
| Clerical "lead" actions (approve an M-Files rev mismatch, move back from VISUAL review) were open to any Clerical member with a lead flag. | REV_APPROVE and moving back from VISUAL review need Clerical Supervisor (PS-Clerical-Supervisor). | There is no Clerical lead AD group. | Yes |
|
||||
| A packet routed to Shipping had no path to filed. | Complete packets routed to Shipping get the same Clerical close and file path. Only the Data Book route needs the VISUAL close before filing. | Shipping packets were stuck at complete. | Yes |
|
||||
| Anyone could set the data book date, even on a filed packet. | SET_DATABOOK_AT is Clerical or Q.C. only (open question 17). It stays allowed on a filed packet. | The data book date is a Clerical and Q.C. record. | Yes |
|
||||
| Change-order verifications were stored in the packet document, with no rule against changing them later. | Verifications are insert-only and cannot be withdrawn (open question 18). | A signed verification is a record. | Yes |
|
||||
| Change-order verifications were stored in the packet document, with no rule against changing them later. | Verifications need a signature, are insert-only and cannot be withdrawn (open question 18). | A signed verification is a record. | Yes |
|
||||
| The old policy did not look at the BOM when the warehouse released a packet. | WH_RELEASE needs every BOM line not marked n/a to be pulled or to have an open short. | A packet should reach the shop with its material, or with the shorts on record. | Yes |
|
||||
| The old screen asked for a heat number on every pulled line before release, but the policy did not check it. | WH_RELEASE also needs a heat on every pulled line that issued pieces, unless the line is N/A or a sub-assembly (`heats_recorded`), checked on the server. | Traceability is the point of the pull. The screen alone could be skipped. | Yes |
|
||||
| Hold reasons and short reasons were any text the screen sent. | Both are closed sets checked on the server. "Other" on a hold needs a note. | The board and the shorts table group and report on the reason, so it must be one of the list. | Yes |
|
||||
@@ -364,3 +500,7 @@ The old policy was looser than the old screens in places. The rebuild follows th
|
||||
| Approving a different M-Files rev was open to a Clerical lead. | rev_approve needs Clerical Supervisor, with a reason and a signature. | There is no Clerical lead AD group, and the approval overrides VISUAL. | Yes |
|
||||
| The Sync on a Clerical PC skipped removed packets ("removed here, not re-made"), but only as the browser saw them. | A removed packet's key is never recreated, whatever VISUAL says. The match runs on the server across live and removed rows, in the feed's transaction. | A removal is a decision. The feed does not undo it. | No |
|
||||
| The Sync saved through the same packet document that held the shop's work, so nothing but the code kept it off the shop's fields. | The feed never writes shop-side tables. Packets past draft only get a change order, observed VISUAL state and drawing flags. | The shop's record is the shop's. | No |
|
||||
| The assign sheet only offered welders of the department and filtered the stations by the welder's limits. The policy checked neither. | ASSIGN checks on the server that the welder has a shop role in the packet's department, and that a limited worker's station is one of their stations. | The picker alone could be skipped. | Yes |
|
||||
| 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 |
|
||||
|
||||
+87
-6
@@ -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 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 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
|
||||
|
||||
@@ -95,10 +95,10 @@ What the warehouse columns mean in practice (phase 4):
|
||||
- `bom_shorts`: one open short per line at most. Opening a short sets the line's `issued` to `qty` minus the short quantity and ticks it pulled. `qty` is what was short in total, `qty_open` what is still missing. `reason` is one of the closed short reasons, `ref` the PO or requisition, `eta` the expected date. Editing an open short keeps what was already received in `qty`. "Not short after all" closes it (`closed_at`) and sets `issued` to the line's `qty`. While a short is open, the line's heat, issued and location change only through the short.
|
||||
- `short_receipts`: one row per delivery, never changed. Each receipt lowers `qty_open`, adds its pieces to the line's `issued` and its heat to the line's `heat`. The receipt that brings `qty_open` to 0 closes the short.
|
||||
- `packet_wh`: `reason` is one of the closed hold reasons when the stage is `hold`, and `note` is up to 200 characters.
|
||||
- `packet_stages`: one row per visit to a shop department. `dept_code`, `station_id`, `welder_id`, `welder_stamp`, `checklist` jsonb (key to `{done, at, by, stamp}`, written per key with `jsonb_set`), `filler`, `filler_lot`, `preheat`, `notes`, the clock (`clock_elapsed_ms`, `clock_running_since`, `clock_started_at`), `qc_snapshot`, `opened_at`, `closed_at`, `row_version`. At most one open stage per packet.
|
||||
- `welds`: `weld_no` (from `next_weld_no`), `page`, `fx` and `fy` (fractions of the page), `wo_base`, `piece`, `items` int[], `type`, `size`, `process`, `stamp`, `welder_id`, `dept_code`, `stage_id`, `carried_from`, `at`, `by`, `row_version`, soft delete. Unique on (`packet_id`, `weld_no`) among live rows.
|
||||
- `heat_tags`: `tag_no` (from `next_heat_no`), `page`, `item`, `fx`, `fy`, `carried_from`, `at`, `by`, `row_version`, soft delete. Unique on (`packet_id`, `tag_no`) among live rows.
|
||||
- `heat_table_placements`: `page`, `x`, `y`, `s`, `by`, `at`. Unique on (`packet_id`, `page`), written as an upsert.
|
||||
- `packet_stages`: one row per visit to a shop department. `dept_code`, `station_id`, `welder_id`, `welder_stamp`, `checklist` jsonb (key to `{done, at, by, stamp}`, written per key with `jsonb_set`), `filler`, `filler_lot`, `preheat`, `notes`, the clock (`clock_elapsed_ms`, `clock_running_since`, `clock_started_at`; only the server writes them, see "The clock"), `qc_snapshot`, `opened_at`, `closed_at`, `row_version`. At most one open stage per packet.
|
||||
- `welds`: `weld_no` (from `next_weld_no`), `page`, `fx` and `fy` (fractions of the page), `bfx` and `bfy` (0006), `wo_base`, `piece`, `items` int[], `type`, `size`, `process`, `stamp`, `welder_id`, `dept_code`, `stage_id`, `carried_from`, `at`, `by`, `row_version`, soft delete. Unique on (`packet_id`, `weld_no`) among live rows.
|
||||
- `heat_tags`: `tag_no` (from `next_heat_no`), `page`, `item`, `fx`, `fy`, `bfx` and `bfy` (0006), `carried_from`, `at`, `by`, `row_version`, soft delete. Unique on (`packet_id`, `tag_no`) among live rows.
|
||||
- `heat_table_placements`: `page`, `x`, `y`, `s`, `carried_from` (0006), `by`, `at`. Unique on (`packet_id`, `page`), written as an upsert.
|
||||
- `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.
|
||||
@@ -109,6 +109,87 @@ What the warehouse columns mean in practice (phase 4):
|
||||
|
||||
Every who column references `users (id)`. Every `signature_id` references `signatures (id)`.
|
||||
|
||||
## Shop (phase 5, migration 0006)
|
||||
|
||||
### Column additions
|
||||
|
||||
- `welds`: `bfx` and `bfy` (double precision, null). The label (bubble) position as fractions of the page. `fx` and `fy` stay where the arrow points.
|
||||
- `heat_tags`: `bfx` and `bfy`, the same.
|
||||
- `heat_table_placements`: `carried_from` (int, null). The packet page number the table was copied from on a change order page replacement.
|
||||
|
||||
A null `bfx` means the label has not been moved. The client then draws it at the old default offset: `bfx = clamp01(fx + (fx > 0.85 ? -0.05 : 0.05))` and `bfy = clamp01(fy + (fy < 0.12 ? 0.07 : -0.07))`, where clamp01 keeps a value between 0.01 and 0.99.
|
||||
|
||||
The API checks the values the columns have no CHECK for:
|
||||
|
||||
| Value | Rule | Refusal |
|
||||
|---|---|---|
|
||||
| `fx`, `fy`, `bfx`, `bfy` | 0 to 1, or null for `bfx` and `bfy` | 422 |
|
||||
| `welds.type` | Fillet, Groove, Fill, Seal or Tack | 422 `invalid_weld` |
|
||||
| `welds.process` | GMAW, FCAW, SMAW, GTAW or SAW | 422 `invalid_weld` |
|
||||
| `welds.size` | `5/16"` on insert when missing | |
|
||||
| `welds.piece` | 1 or more | 422 |
|
||||
| `welds.wo_base` | one of the packet's work order bases | 422 `invalid_wo` |
|
||||
| `heat_table_placements.x`, `y` | 0 to 0.97 | 422 `invalid_heat_table` |
|
||||
| `heat_table_placements.s` | 0.6 to 2.1, in steps of 0.15 from the screen | 422 `invalid_heat_table` |
|
||||
| `page` of a heat table placement | a live or voided drawing page of the packet | 422 `invalid_page` |
|
||||
| `page` of a new or moved weld or heat tag | not a voided page | 422 `void_page` |
|
||||
|
||||
### Checklist templates
|
||||
|
||||
The `settings` row `checklist_templates` holds the shop checklists. Migration 0006 inserts the defaults with `ON CONFLICT DO NOTHING`, so a row already there is kept. The value:
|
||||
|
||||
```json
|
||||
{
|
||||
"weld": [{"group": "Before welding", "key": "dwg", "text": "Drawing {drawing} Rev {rev} matches the packet"}, ...],
|
||||
"plain": [{"group": "Before starting", "key": "dwg", "text": "..."}, ...]
|
||||
}
|
||||
```
|
||||
|
||||
`weld` is the list for a weld department (`departments.weld` true), `plain` for every other shop department. Each entry has a `group`, a `key` and a `text`. The text may hold the placeholders `{drawing}`, `{rev}` and `{wps}`, filled from the packet.
|
||||
|
||||
| List | Group | Keys |
|
||||
|---|---|---|
|
||||
| weld | Before welding | `dwg`, `heat`, `wps`, `fit`, `pre` |
|
||||
| weld | After welding | `vt`, `stamp`, `clean`, `qty` |
|
||||
| plain | Before starting | `dwg`, `prev`, `mat` |
|
||||
| plain | When done | `work`, `insp`, `qty` |
|
||||
|
||||
When the row is missing or malformed, the defaults in `domain/checklists.py` apply and a warning is logged. The read model and the checklist write both use the loaded templates. No screen edits the row in phase 5. Phase 9 (Admin) adds that.
|
||||
|
||||
`packet_stages.checklist` maps each key to `{done, at, by, stamp}`. `by` is the user id. `stamp` is the user's `welder_stamp`, else the packet's. A key not in the department's list is 422.
|
||||
|
||||
### Carried marks
|
||||
|
||||
Welds, heat tags and heat table placements belong to the packet, not to the stage. Routing a packet to another department copies nothing. The read model computes a `carried` flag instead:
|
||||
|
||||
- A weld is carried when `stage_id` is set and is not the open stage's id. It was logged in an earlier department's stage.
|
||||
- A heat tag is carried when its `at` is before the open stage's `opened_at`. The warehouse or an earlier department placed it.
|
||||
|
||||
The `carried_from` columns on `welds` and `heat_tags` (from 0003) are not written by routing.
|
||||
|
||||
There is one copy, on a change order page replacement. When a feed change order is approved and a drawing page is voided, each heat table placement on the voided page is copied to its new page with `carried_from` set to the old page number. The log says `Heat table copied to new sheet` with the note `from the VOID copy, check its position on the new revision`. Welds and heat tags stay on the void page.
|
||||
|
||||
### The clock
|
||||
|
||||
`packet_stages` has three clock columns (from 0003):
|
||||
|
||||
- `clock_elapsed_ms` (bigint, default 0): the stored total.
|
||||
- `clock_running_since` (timestamptz): when the clock last started. Null while it is stopped.
|
||||
- `clock_started_at` (timestamptz): the first start of the stage.
|
||||
|
||||
Only the server writes them, with the database `now()`. No request field sets them, and a value from the client is never used.
|
||||
|
||||
| Event | Write |
|
||||
|---|---|
|
||||
| START, or clock `start` | `clock_running_since = now()` when it is not running; `clock_started_at = coalesce(clock_started_at, now())` |
|
||||
| clock `pause` | `clock_elapsed_ms += now() - clock_running_since`; `clock_running_since = null` |
|
||||
| clock `resume` | `clock_running_since = now()` |
|
||||
| PULL_BACK, UNASSIGN, SUBMIT | fold: `clock_elapsed_ms += now() - clock_running_since` when running; `clock_running_since = null` |
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
@@ -149,7 +230,7 @@ The app role is refused by privilege first. The migrate role owns the tables, so
|
||||
## 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"). 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"). A missing key takes its default. A save bumps `version` and sends `ps_notify('ps_settings', ...)` so the worker reschedules.
|
||||
|
||||
Phase 3, migration 0004:
|
||||
|
||||
|
||||
+12
-2
@@ -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 4. 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. Every other control names the phase that builds it.
|
||||
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.
|
||||
|
||||
Contents:
|
||||
|
||||
@@ -99,6 +99,16 @@ The M: drive build had 20 layers of protection, most of them enforced in the bro
|
||||
- Short receipts go to the warehouse in any status but filed, or to the holder of the packet through the BOM edit rule. Nobody else.
|
||||
- An MTR on a line must be a PDF, PNG or JPEG asset.
|
||||
|
||||
### Shop rules (phase 5)
|
||||
|
||||
- The server is the clock of record. The station clock is kept in `packet_stages` with the database time. Only the server writes the clock columns. No request field sets the elapsed time.
|
||||
- Weld and heat tag numbers (`W<n>`, `H<n>`) are assigned by the server from the packet counters, under the packet row lock. Two people marking at once never get the same number.
|
||||
- ASSIGN checks on the server that the welder belongs to the department and that a limited worker's station is one of theirs.
|
||||
- A change-order verification needs a signature and is insert-only. A second one for the same department is refused. It cannot be withdrawn.
|
||||
- VOID sheets are never edited. A voided page is kept, and new marks or moves on it are refused (422 `void_page`).
|
||||
- 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.
|
||||
|
||||
### 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.
|
||||
@@ -171,7 +181,7 @@ The IDs come from Part 3 of the old SECURITY.md. The status follows the approved
|
||||
| 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 |
|
||||
| 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. |
|
||||
| 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