# Packet Station policy Who may do what to a packet, and when. This file is written from the code in `server/packetstation/policy/`. When they disagree, the code is the rule and this file is out of date. ## Purpose The policy is a Python port of the old `guard.js`. It runs only on the server. Every packet write calls `policy.check()` inside the write transaction, under the packet row lock. Every packet read returns `can`, `blocked` and `holder` from the same rules, so the screens only show what the server will accept. The package is pure functions with no I/O: - `actions.py`: the `Action` enum, the `Who` predicates, the `Rule` record and the `RULES` table. - `principal.py`: `Principal`, `PacketState`, `principal_from_roles()`. - `custody.py`: `holds()`, `works_on()`, `holder_name()` and the Who predicates. - `preconditions.py`: the named preconditions and their messages. - `engine.py`: `check()`, `evaluate()`, `can()` and `Decision`. ## Roles Every role comes from an AD group (see docs/SECURITY.md). The role strings are `admin`, `clerical`, `clerical_supervisor`, `warehouse`, `warehouse_supervisor`, `qc`, `qc_supervisor`, `pm` and `shop::`. A person's roles are the union of their groups. `principal_from_roles(user_id, roles, station_limits=(), session_kind="password")` builds the policy's `Principal`: | Field | Meaning | |---|---| | `admin` | In PS-Admins. Admin is not membership. It is the bypass rule below. | | `support` | The codes among CL, WH and Q.C. the person belongs to, at any level. A supervisor role implies membership. | | `support_sup` | The codes among CL, WH and Q.C. where the person is Supervisor. | | `shop` | Shop department code to level: 1 worker, 2 lead, 3 supervisor. The highest level wins. | | `station_limits` | Station ids a worker is limited to. Empty means any station. | | `pm` | Project Management. PM can read every packet and change nothing. | | `session_kind` | `password` or `pin`. | | `system` | `visual-feed` for the worker. Never set from a session. | A role string that does not parse is ignored. ## Custody Only whoever holds the packet may change it. Everyone else can open it and follow it. | Status | Holder | |---|---| | draft | Clerical | | warehouse | Warehouse | | ready, review | Lead or supervisor of the packet's department | | assigned, in_progress | Lead or supervisor of the department, or a worker who works on it | | qc | Q.C. | | complete | Clerical (data book) | | filed | Nobody | `works_on(principal, state)`: a member of the packet's department. Leads and supervisors always pass. A worker passes when they have no station limits, or the limits hold the packet's station. START and SUBMIT also need the assigned welder for a worker (see the tightenings). `holder_name(state, dept_name, station_name)` gives the text for messages: "Clerical", "Warehouse", "Q.C.", "Clerical (data book)", "the data book (filed)", else the department name, plus " · " while the packet is at a station. Statuses in workflow order: draft, warehouse, ready, assigned, in_progress, review, qc, complete, filed. SHOP is ready, assigned, in_progress and review. ACTIVE is assigned and in_progress. ## Who | Who | Passes when | |---|---| | CL, WH, QC | A member of that support department, any level | | CL_SUP, WH_SUP, QC_SUP | Supervisor of that support department | | CL_OR_QC | A member of Clerical or Q.C. | | HOLDER | Holds the packet (custody above) | | DEPT_LEAD | Lead or supervisor in the packet's department | | DEPT_SUP | Supervisor in the packet's department | | WORKS_ON | Lead or above in the department, or a worker in the department who is the assigned welder and whose station limits allow the station | | CO_VERIFIER | Supervisor of the department named in the request: a shop Supervisor for a shop code, WH or Q.C. Supervisor for WH or QC. Clerical never verifies. | | ADMIN | In PS-Admins | | SYSTEM | The worker (`system` is set) | ## Rule table `from` is the set of statuses the action starts from. "any" means any status, with the filed lock applied separately. `co hold` means an open change order holds the step. | Action | Who | From | To | Dept | CO hold | Preconditions | Signature | Reason | |---|---|---|---|---|---|---|---|---| | send_to_warehouse | CL | draft | warehouse | no | | heat_map_answered, drawing_attached | | | | release_to_shop | CL | draft | ready | allowed | | heat_map_answered, drawing_attached, target_dept_given | | | | send_draft_to_qc | CL | draft | qc | no | | heat_map_answered, drawing_attached | | | | wh_send_back | WH | warehouse | draft | no | | reason_given | | yes | | return_to_clerical | DEPT_LEAD | ready, only when wh_skipped | draft | no | | reason_given | | yes | | wh_release | WH | warehouse | ready | no | yes | bom_all_pulled_or_short, heats_recorded | | | | send_back_to_warehouse | DEPT_LEAD | ready | warehouse | no | | reason_given | | yes | | assign | DEPT_LEAD | ready | assigned | no | | welder_set | | | | unassign | DEPT_LEAD | assigned | ready | no | | | | | | pull_back | DEPT_LEAD | in_progress | ready | no | | | | | | start | WORKS_ON | assigned | in_progress | no | | | | | | submit | WORKS_ON | assigned, in_progress | review | no | yes | checklist_complete, welds_recorded, filler_set | Submit work to the supervisor | | | return_to_station | DEPT_SUP | review | assigned | no | | reason_given | | yes | | send_to_qc | DEPT_SUP | review | qc | no | yes | qc_book_spool_location | Send to Q.C. | | | route_from_review | DEPT_SUP | review | ready | required | yes | target_dept_given | Route to another department | | | qc_reject | QC | qc | review | no | | reason_given | Q.C. reject | yes | | qc_accept | QC | qc | complete | no | yes | qc_missing_empty, route_given | Q.C. accept | | | qc_accept_and_route | QC | qc | ready | required | yes | qc_missing_empty, target_dept_given | Q.C. accept and route | | | file | CL | complete | filed | no | yes | vclosed_if_databook | File in the data book | | | admin_reopen | ADMIN | filed (filed_ok) | complete | no | | reason_given | Reopen a filed packet | yes | | edit_header | CL | draft | | | | | | | | edit_drawing_ref | CL | any | | | | | | | | edit_dept | CL | draft | | | | | | | | move_unit | CL | any | | | | | | | | edit_wos | CL | draft | | | | | | | | edit_bom | HOLDER | draft, warehouse, SHOP | | | | | | | | wh_receive_short | WH | any | | | | short_open | | | A holder of the packet who is not in the warehouse receives a short through edit_bom (see "Receive by custody"). | edit_heats | HOLDER | warehouse, SHOP | | | | | | | | edit_heat_table | HOLDER | warehouse, SHOP | | | | | | | | edit_welds | HOLDER | SHOP | | | | | | | | edit_checklist | HOLDER | SHOP | | | | | | | | edit_stage_fields | HOLDER | SHOP | | | | | | | | clock | HOLDER | SHOP | | | | | | | | edit_qc | QC | qc | | | | | | | | set_wh_stage | WH | warehouse | | | | | | | | edit_document_set | CL | any | | | | | Attach a drawing by hand (attach only) | | | doc_add | CL_OR_QC | any | | | | | | | | doc_remove | CL_OR_QC | any | | | | | | | | rev_approve | CL_SUP | draft | | | | reason_given | Approve the M-Files revision | yes | | set_vclosed | CL | complete, only when routed_to is databook | | | | | | | | set_databook_at | CL_OR_QC | any (filed_ok) | | | | | | | | remove | CL | draft | | | | reason_given | | yes | | co_issue | CL | any | | | | | Issue a change order | | | co_approve | CL | any | | | | | Approve a change order | | | co_reject | CL | any | | | | reason_given | Reject a change order | yes | | co_hold_toggle | CL | any | | | | | | | | co_verify | CO_VERIFIER | any | | | | | Verify a change order for the department | | | itp_edit_line | QC | any | | | | | | | | itp_block_sign | QC | any | | | | | Sign an ITP block | | | itp_sign_off | QC | any | | | | | Sign off the ITP | | | itp_reopen | QC_SUP (Admin through the bypass) | any | | | | reason_given | | yes | | vis_update_draft | SYSTEM | draft | | | | | | | | vis_retire_draft | SYSTEM | draft | | | | | | | | vis_open_co | SYSTEM | any but draft and filed | | | | | | | | vis_auto_release | SYSTEM | draft | warehouse | | | heat_map_answered, drawing_attached | | | The `vis_*` actions are system only. People can never do them, and the worker can do nothing else. ## Preconditions Each precondition is a named predicate over the packet state and the request input. They are never bypassed, Admin included. The first one that fails gives the message. | Name | Passes when | Message | |---|---|---| | heat_map_answered | heat_map is yes or no | Say whether this packet needs a heat map. | | drawing_attached | a drawing is attached | Attach the drawing first. | | bom_all_pulled_or_short | every line not marked n/a is pulled or has an open short | Pull every material line or mark it short first. | | heats_recorded | no line needs a heat: every line that is not N/A, not a sub-assembly, is pulled and issued pieces has a heat | Scan or type the heat number of every pulled line first. Use N/A for a part with no traceability. | | 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 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). | | reason_given (input) | a reason of at least 8 characters after cleaning | Give a reason of at least 8 characters. | | vclosed_if_databook | routed to Shipping, or closed in VISUAL | Close the job in VISUAL first. | | co_staged_if_dwg_change (input) | no drawing change, or the new drawing is staged (phase 3) | Stage the new drawing for the change order first. | | route_given (input) | routed_to is databook or shipping | Choose Data Book or Shipping. | | target_dept_given (input) | a shop department different from the packet's; from a draft any shop department | Choose a department. / Choose a different department. | "(input)" marks a precondition that reads request input, not packet state. `can()` skips those. ## Order of checks `check(principal, state, action, *, extra={}, it_reason=None) -> Decision` runs these in this order and stops at the first failure: 1. **Removed.** A removed packet refuses everything, Admin included. Code `removed`. 2. **Status.** The status must be in the rule's from set. RETURN_TO_CLERICAL also needs wh_skipped. SET_VCLOSED also needs routed_to databook. An unknown status fails here too. Code `wrong_status`. 3. **Change-order hold.** A held action refuses while an approved change order waits for verification, or a pending one is on hold. Admin included. Code `co_hold`, with the hold text. 4. **System.** A system-only action refuses every person (`system_only`). The system principal passes only system-only actions (`forbidden`). 5. **Admin bypass.** An admin who is not allowed by the rule's Who passes who and custody when an IT reason of at least 8 characters is given. Without one the code is `it_reason_required`. SET_DATABOOK_AT needs no reason. The decision is marked `admin_bypass`, and the write marks the log row as an IT edit. 6. **Filed lock.** A filed packet refuses everything except SET_DATABOOK_AT and ADMIN_REOPEN, Admin included. Code `filed`: "This packet is filed and locked. IT can reopen it if a correction is needed." 7. **Who and custody.** Code `forbidden`. For a step: "Your access doesn't allow this step." For an edit: "Your access doesn't allow changing (what) on this packet. It's with (holder) right now." PM fails here for everything. 8. **Preconditions,** in rule order, never bypassed. Code `precondition` with the message, or `reason_required` when the missing piece is the rule's reason, so the screen can ask for one. `Decision` carries `ok`, `code`, `why`, `admin_bypass`, `signature` (the meaning text when the rule needs one) and, in read mode only, `reasons`. The packet API answers 403 with the code for most failures, and 400 for `it_reason_required` and `reason_required`. ## Signatures These actions need a signature: submit, send_to_qc, route_from_review, qc_reject, qc_accept, qc_accept_and_route, file, admin_reopen, edit_document_set (attach only), rev_approve, co_issue, co_approve, co_reject, co_verify, itp_block_sign and itp_sign_off. The rule's signature text is the meaning that is signed. The request carries `sign: {factor, secret}`. The write pipeline calls the phase 1 helper `packetstation.auth.signatures.sign()` inside the same transaction, after the policy passes. It checks the PIN or domain password against the session's own user. A PIN works in any session. It logs the attempt and applies the five-strike PIN disable and the three-strike session end. It writes an append-only `signatures` row with the user, session, account, display name, session kind, factor, action, meaning, packet, subject and a sha256 over the canonical JSON of what was signed (the action, the packet id, the expected state and the request fields). The `packet_transitions` and `packet_log` rows point at it. ## Concurrency - **Workflow steps** are compare-and-set on `expect: {status, dept}` under the packet row lock (`SELECT ... FOR UPDATE`). When the packet moved on, the answer is 409 `stale_step` "Someone else already took this step." with the current status, department and version. There is no packet version check on a step, so a weld edit never blocks a send. Side effects run in the same transaction. - **Keyed child rows** (BOM lines, Q.C. inspections, stage fields, welds, heat tags) take `If-Match: `. A mismatch is 409 `row_conflict` with the current row. - **Numbers** for welds, heat tags, pages and change orders come from counters on the packet row under the row lock, so concurrent adds never collide. - **Header scalars** use field-level compare-and-set: the body sends `set` and `base`. A key whose base differs from the current value is 409 `field_conflict` with the current value. A key with no base is written as is. - **Work orders** are a draft-only full replace with If-Match on the packet version. - **Shorts and change-order actions** are state compare-and-set. Checklist keys are written one key at a time. The heat table position is an idempotent upsert. A change-order verification is an insert, so a second one is "already verified". - **Append-only tables** never conflict: packet_transitions, packet_log, qc_history, short_receipts, project_renumbers, itp_rejects and co_verifications. Triggers refuse UPDATE, DELETE and TRUNCATE, and the app role has no grant for them. ## Reads: can, blocked and holder Every packet read returns: - `can`: the actions that pass now. Input-only preconditions are skipped, because they are what the user gives with the step. - `blocked`: the actions that pass who and custody but fail a state precondition or the change-order hold, with every reason. The screen shows the control disabled with the reason. - `holder`: `holder_name()` for the packet. `can(principal, state, assume_it_reason=True) -> (list[Action], dict[Action, list[str]])`. For an admin the IT reason is assumed, so the list shows what the admin may do once they give one. `evaluate(principal, state, action)` returns the read-mode decision with `admin_bypass` set, so the screen knows to ask for the reason. System-only actions are never listed. CO_VERIFY needs a department in the request, so it is not listed; the change order list says which departments the person can verify. ## Warehouse (phase 4) ### Stages The warehouse stage is `packet_wh`, separate from the packet status. SET_WH_STAGE (WH, status warehouse) moves it, compare-and-set on `expect_stage` (409 `stale_step` when it moved on). | From | To | |---|---| | incoming | pulling, hold | | pulling | pulled, hold | | hold | pulling | | pulled | pulling | A move to the same stage, or one not in the table, is 409 `wh_transition`. A packet with no `packet_wh` row is at incoming. The first Pulled tick on a line while the stage is incoming or on hold starts pulling by itself (`auto` true). ### Hold reasons A closed set, checked on the server: - Waiting on material (not in stock) - Waiting on purchasing / PO - Question for engineering or clerical - Drawing / BOM problem - Other Any other text is 400 `invalid_reason` "Pick one of the hold reasons." "Other" needs a note, else 400 `reason_required` "Say what it's waiting on." The note is cleaned and cut to 200 characters. ### Short reasons A closed set, checked on the server: - Not in stock - On order - Back-ordered - Damaged / rejected - Wrong part received - Needs cutting / prep - Other Any other text is 400 `invalid_reason` "Pick one of the short reasons." A short has a quantity from 1 to the line quantity, a reason, an optional PO or requisition, an optional expected date, an optional note, and the optional heat of the pieces that were pulled (never on an N/A line). Opening, editing and clearing a short is edit_bom, so the holder decides. In practice that is the warehouse. ### The release gate WH_RELEASE checks two preconditions, in this order: 1. `bom_all_pulled_or_short`: every line not N/A is pulled or has an open short. 2. `heats_recorded`: no line needs a heat. A line needs a heat when it is not N/A, not a sub-assembly (`sub` is `Y`; its heat is on its own packet), is pulled, has issued more than 0 and has no heat. Both are state preconditions, so `blocked.wh_release` lists the failing message and the screen shows Release disabled with it. Heat numbers are checked against the old pattern: letters, numbers, space . / # _ - only, up to 40 characters, starting with a letter or a number. A bad heat is 422 `invalid_heat`. ### Receive by custody Receiving short parts (`POST .../bom/{item}/short/receipts`) picks its action from the person: - A member of the warehouse (WH in `support`) receives through WH_RECEIVE_SHORT, in any status but filed, while the line has an open short. The warehouse does not need to hold the packet. - Anyone else receives through EDIT_BOM, so only the holder may: the lead or supervisor of the department that has the packet, Q.C. at Q.C., Clerical in draft. - Everyone else is refused by the policy (403 `forbidden`). The warehouse still cannot change any other field of a packet it no longer holds. EDIT_BOM stays the holder's. A receipt needs a heat unless the line is N/A, and its quantity is 1 to what is still open. ### MTR files The MTR on a line must be an asset whose type is PDF, PNG or JPEG. Anything else, or an asset that is not there, is 422 `invalid_asset`. Clearing the MTR is allowed. ### Packet log texts The `action` column of `packet_log` for each warehouse write. `{item}` is the BOM item number. | Write | action | note | |---|---|---| | Heat typed or scanned | `Heat recorded · item {item}` | the heat | | Heat cleared | `Heat cleared · item {item}` | | | Pulled ticked | `Pulled · item {item}` | | | Pulled unticked | `Pulled undone · item {item}` | | | N/A ticked | `Marked N/A · item {item}` | | | N/A unticked | `Traceable again · item {item}` | | | Issued only | `Issued qty changed · item {item}` | `{issued} of {qty}` | | Location only | `Location set · item {item}` | the location | | Several fields | `Line changed · item {item}` | | | MTR attached | `MTR attached · item {item}` | | | MTR removed | `MTR removed · item {item}` | | | Short opened | `Short recorded · item {item}` | `{qty} of {need} short · {reason} · {ref} · ETA {eta}`, parts present only | | Short edited | `Short updated · item {item}` | the same as recorded | | Not short after all | `Short cleared · item {item}` | | | Short received | `Short received · item {item}` | `{qty} pc · heat {heat} · {still} still short` or `... · short closed`, then ` · packet {status}` (lower case) and ` in {station}` when at a station | | Stage incoming to pulling | `Warehouse: Started pulling` | | | Stage to hold | `Warehouse: Put on hold` | `{reason} · {note}` | | Stage pulling to pulled | `Warehouse: Marked pulled, ready for production` | | | Stage hold to pulling | `Warehouse: Pulling resumed` | | | Stage pulled to pulling | `Warehouse: Back to pulling` | | | wh_release | `Released to supervisor`, or `Released to supervisor with {n} short` when shorts are open | `{heats} heats recorded`, plus ` · SHORT: item {item} ×{qty_open} ({reason}), ...` when shorts are open | | wh_send_back | `Sent back to Clerical` | the reason | 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-` 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` and `H` 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 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: `, `Unchecked: ` | | | 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.`, role `system`) and a change event. See [FEED.md](FEED.md) for when each write happens. | Action | From | What the worker does | |---|---|---| | vis_update_draft | draft | Updates a draft made by the feed: work orders, BOM (unless uploaded by hand), drawing and rev, dates, flags, drawing pages from M-Files. | | vis_retire_draft | draft | Soft-removes an untouched draft whose work orders all moved to another packet, reason `moved`. | | vis_open_co | any but draft and filed | Opens, re-aims or withdraws the packet's one pending feed change order, stages a drawing on it, and records drawing flags on a packet past draft. | | vis_auto_release | draft to warehouse | Sends a ready draft to the warehouse. Same preconditions as send_to_warehouse (heat map answered, drawing attached). The feed only picks drafts with no flags and the rev confirmed by the drawing job. | What the feed may write: - Drafts it made, in place, through vis_update_draft and vis_retire_draft. - New drafts, projects and units (`created_by` null, audit role `system`). - The order-derived fields of projects and units, only where the value came from VISUAL. A value Clerical typed is never overwritten. - On packets past draft: only a change order (vis_open_co), the observed VISUAL state (leg statuses, closed, cancelled, work order gone, ticket hours) and the drawing flags. - Its own tables: the caches, `drawing_lookups`, the job tables. What the feed may never write: - Shop-side tables: stages, welds, heat tags, checklists, the clock, warehouse pulls and shorts, Q.C. records, seals. In-shop packets only get a change order. - A removed packet. A key whose packets are all removed is never made again. - A filed packet. - Any person's action. The system principal passes only the vis_* actions, and people can never take them. - VISUAL or M-Files. Both libraries are read-only. ### Rev approval When M-Files has a different released rev from VISUAL, the drawing job stages the M-Files copy. A Clerical Supervisor (PS-Clerical-Supervisor) can approve it on a draft with rev_approve: a reason of at least 8 characters and a signature ("Approve the M-Files revision"). The pages are attached and the approval is kept on the packet (`visual.rev_ok`). With nothing staged the answer is 409 `nothing_staged`. ### Hand uploads on flagged drafts On a draft made by the feed that is flagged (or already has a hand upload), Clerical can upload a drawing (edit_document_set) or a BOM (edit_bom) by hand. Both need a reason and a signature. A hand drawing sets `drawing_state` `hand` and flag `hand_drawing`; a hand BOM sets flag `hand_bom` and the feed stops replacing the BOM. An unflagged draft refuses with 409 `not_flagged`. Auto-release never takes a hand-flagged draft, so a person sends it on. "Use VISUAL's BOM again" removes the hand BOM. ### Change orders from the feed - A pending feed change order holds nothing, unless Clerical puts it on hold (co_hold_toggle). Then it blocks the held steps until released or decided. - Approval (co_approve, Clerical, signature) applies the staged change to the packet: work orders, BOM, drawing and rev. When the drawing changed, the new drawing must be staged first (`co_staged_if_dwg_change`, 409 `co_not_staged`). The old drawing pages are voided, not deleted. - After approval a major change order asks the departments it touches to verify (co_verify), and holds the packet until they have. - A minor change order (only target hours or the want date changed) needs Clerical approval only. No department is asked to verify, so it holds nothing after approval. - Rejection (co_reject) needs a reason and a signature. The feed does not reopen a change order Clerical rejected for the same target. ## Tightenings against the old build The old policy was looser than the old screens in places. The rebuild follows the screens where they were stricter, and the README where it states the intent. Each row is a change Seth can veto. | What the old build allowed | What the rebuild does | Why | Can be vetoed | |---|---|---|---| | A department lead could write welds, BOM, heat tags, the checklist, stage fields and the clock through the policy whenever the packet was in a shop status. | These edits need the holder of the packet (the holder rule, applied as the old screens did). A lead of another department cannot. | The screens only offered these to the holder. The policy now matches. | Yes | | Any worker in the department within their station limits could start or submit a packet (`worksOn` ignored who was assigned). | START and SUBMIT need the assigned welder for a worker. Leads and supervisors still can. | The README says only the assigned worker. | Yes | | A status change could carry any other field in the same save. | A transition carries only its own fields (the step's inputs). Other changes go through their own edit actions and rules. | One rule per write keeps the check honest. | No | | Clerical "lead" actions (approve an M-Files rev mismatch, move back from VISUAL review) were open to any Clerical member with a lead flag. | REV_APPROVE and moving back from VISUAL review need Clerical Supervisor (PS-Clerical-Supervisor). | There is no Clerical lead AD group. | Yes | | A packet routed to Shipping had no path to filed. | Complete packets routed to Shipping get the same Clerical close and file path. Only the Data Book route needs the VISUAL close before filing. | Shipping packets were stuck at complete. | Yes | | Anyone could set the data book date, even on a filed packet. | SET_DATABOOK_AT is Clerical or Q.C. only (open question 17). It stays allowed on a filed packet. | The data book date is a Clerical and Q.C. record. | Yes | | Change-order verifications were stored in the packet document, with no rule against changing them later. | Verifications need a signature, are insert-only and cannot be withdrawn (open question 18). | A signed verification is a record. | Yes | | The old policy did not look at the BOM when the warehouse released a packet. | WH_RELEASE needs every BOM line not marked n/a to be pulled or to have an open short. | A packet should reach the shop with its material, or with the shorts on record. | Yes | | 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 | | A hand draft could leave Clerical with no drawing (the packet PDF upload stood in for it). | A hand draft needs a drawing attached before any send from Clerical. | There is no packet PDF upload in the rebuild. | Yes | | Attaching drawing pages by hand needed no signature. | Attaching a drawing by hand needs a signature ("Attach a drawing by hand"). | It replaces what M-Files would supply, so a person stands behind it. | Yes | | Admin could write anything on a filed packet with an IT reason. | Admin cannot write a filed packet except through ADMIN_REOPEN (with a reason and a signature), and SET_DATABOOK_AT. | A filed packet is sealed. A change breaks the seal on purpose, on record. | No | | Admin passed every check after the change-order hold, so an admin could make a step from any status. | Admin goes through the same status check. The bypass covers who and custody only. Preconditions still apply. | Admin fixes data, not the workflow order. | Yes | | A hand drawing or BOM upload on a flagged VISUAL draft took a reason of 3 characters and a PIN sign-off checked in the browser. | Both need a reason of at least 8 characters and a signature verified on the server, kept on the packet with who and when. | A hand upload stands in for VISUAL or M-Files, so a person stands behind it. | Yes | | 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 |