Files
PacketStation/docs/PLAN.md
T
spencermandClaude Opus 5.5 77d40f6c05
ci / frontend (push) Successful in 44s
ci / server (push) Successful in 10m45s
ci / image (push) Successful in 33s
ci / e2e (push) Successful in 3m58s
docs: apply the closed Gitea decisions 3, 19, 20 and 38
- Issue 3 (Q4b): the registered-device cookie stays; no device certificate.
- Issue 19 (Q22): upload limits stay; ClamAV stays off in both stacks,
  the workstations run Microsoft Defender.
- Issue 20 (Q23): the storage alert stays at 85 percent; phase 9 mails
  it to PS-Admins.
- Issue 38 (OPS2): the test stack stays under ~/packetstation/test on
  docker-1. test.env.example, the compose header, README, CLAUDE.md,
  DEPLOY.md and SECURITY.md give that path and the actual modes. The
  production layout is asked on issue 38.

All four confirm the built defaults, so no code changed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:11:06 -06:00

291 lines
52 KiB
Markdown

> Approved 2026-10-15. Copied verbatim from the approved plan file so later sessions have it.
# Packet Station rebuild: phased plan
## Context
Packet Station runs today as a browser-only app on the M: drive (plain JavaScript, PowerShell sync, files as locks and records). It has 720 real packets in it, all still at draft or warehouse. The brief (docs/REBUILD-PROMPT.md in the old repo) asks for a from-scratch production web application: Docker Compose on docker-1, PostgreSQL 16, a Python server that enforces every rule, the browser as a pure client, VISUAL through VisualKit and M-Files through MFilesKit. Feature parity with the M: install is the acceptance bar, minus the "Drop" list. No data migration: the new system starts empty behind a go-live boundary date.
This plan was built from a full read of the old repo (README, guard.js, sync.js, visual.js, packet_rules.py, databook.js, print.js, itp.js, auth.js, security.js, depts.js, SECURITY.md, PRODUCTION-PLAN.md, the AD scripts, the 38 Playwright scenarios), the live record shapes on M:, VisualKit (CLAUDE.md, INSTALL.md, PLAN.md 8b/8c/9.7, the feed example and its API) and MFilesKit (INSTALL.md, packet-station.md, drawings.py). Facts from those reads that change the design are called out where they matter.
## Where the new repo lives
The brief says start in a new, empty folder. This session is rooted in the old repo, which stays read-only. Proposed: `C:\Users\spencerm\Coding\PacketStation2` (its own Gitea repo later). The first action after approval is to add that folder to the session and `git init` there. Nothing in the old repo is modified; the old repo's untracked docs (PRODUCTION-PLAN.md, REBUILD-PROMPT.md, docs/ad) are left for Spencer to commit.
## Decisions taken as given (from the brief)
Python 3.12 + FastAPI + uvicorn; psycopg 3; PostgreSQL 16 with plain SQL migrations and three database roles (superuser for bootstrap, migrate, app); relational core with JSONB at the edges; LDAP sign-in with ldap3 and roles from the 50 PS-* AD groups; optional server-side Packet Station PIN (Argon2id) for registered devices, unlock and signatures; SSE over LISTEN/NOTIFY; server-authoritative sessions, locks and timeouts; files on a volume with metadata in PostgreSQL; append-only audit; HMAC seals; test and production stacks from one image; no published port, NPM on npm_default; worker on APScheduler; server-side PDFs; TypeScript + React + Vite frontend with the PESCO dark theme; no CDN.
Challenges to the brief, after reading the code (none change the architecture):
1. **The old policy is looser than the old UI in places** (leads may write welds and BOM in shop statuses; `worksOn` ignores who is assigned; a transition patch may carry any field). The port follows the UI where the UI is stricter, and the README where it describes intent ("only the assigned worker"). Each such tightening is listed in docs/POLICY.md so Seth can veto one.
2. **Clerical "lead" has no AD group.** The brief maps it to PS-Clerical-Supervisor. The old code gates rev-mismatch approval and "move back from VISUAL review" on CL lead; both move to Clerical supervisor. CO verification already needs supervisor level, which the shop Supervisor groups give.
3. **"Shipping" route has no path to filed** in the old build. The rebuild gives complete packets routed to Shipping the same Clerical close and file path (vclosed warning instead of block applies to all routes). Flagged for Seth.
4. **VisualKit's feed takes "first open order, newest" per project.** The rebuild stores every open order per project (orders_by_project) and picks the order whose `top_jobs` or `lines` includes the packet's work order base when it can, else the project default, and shows the source. Open question 5 covers the rest.
5. **Labor tickets are not in the feed example.** Adding them is a new `v.labor.tickets_for_work_orders` read in the worker, gated by the timer decision (open question 1).
6. **MFilesKit's docs are stale in two lines** (drafting rev precedence and the project-folder rule); the code is the rule. docs/FEED.md states the code behaviour.
## Target repository layout (PacketStation2)
```
CLAUDE.md README.md CHANGELOG.md pyproject.toml Dockerfile .env.example
docs/ SECURITY.md DEPLOY.md FEED.md POLICY.md SCHEMA.md GO-LIVE.md PAPER-FALLBACK.md
server/packetstation/
main.py (FastAPI app) settings.py db.py (psycopg pool, roles) migrations/ (NNNN_name.sql)
auth/ (ldap, sessions, pin, devices, ratelimit) policy/ (actions, custody, transitions, preconditions)
domain/ (packets, projects, warehouse, shop, qc, itp, changes, docs, assets, admin, pm)
api/ (routers per resource; events.py for SSE) pdf/ (databook, traveler, itp, prints, stamps)
worker/ (scheduler, jobs: visual_feed, drawing_lookup, ad_reconcile, integrity, backup_check, digest)
feed/ (visualkit adapter, packet_rules, fingerprint, orders) drawings/ (mfileskit adapter)
notify/ (smtp, routing) cli.py (doctor, migrate, worker, serve, fixtures)
frontend/ (Vite + React + TS; src/theme, src/screens/{clerical,warehouse,supervisor,stall,qc,pm,admin,account}, src/packet/*)
tests/server/ (pytest: policy matrix, merge rules, packet rules, feed fixtures, auth)
tests/e2e/ (Playwright scenarios, synthetic fixtures) tests/fixtures/generator.py
deploy/ compose.prod.yml compose.test.yml compose.ci.yml npm/ (proxy host notes) samba-ad/ (CI DC seed)
scripts/ backup.sh restore.sh packetstation-backup.service/.timer
.gitea/workflows/ci.yml
```
## Phases
Each phase ends with passing tests, docs updated (README, CHANGELOG, SECURITY.md, DEPLOY.md, FEED.md as applicable) and a commit. Opus 5.5 subagents build each phase's pieces in parallel where files do not overlap (server vs frontend vs tests); the main session integrates and runs the suite. Sizes are rough and relative.
### Phase 0: Skeleton, image, doctor (small)
- New repo, CLAUDE.md in VisualKit's shape (safety rules, layout, running things, related projects: PacketStation on M:, VisualKit, MFilesKit, pesco-ncr), README, CHANGELOG, docs/ skeleton.
- Multi-stage Dockerfile: python:3.12-slim-bookworm, Microsoft Debian 12 repo for msodbcsql18, pip from the Gitea index with `--index-url`, frontend build stage, non-root user.
- `packetstation doctor`: runs `visualkit doctor` and `mfileskit doctor`, checks DB roles and migrations, assets volume, LDAP bind and that the service account reads memberOf, employeeID, userAccountControl on both controllers, SMTP STARTTLS verify. This is the first real Linux test of both libraries; it gates everything after.
- Migration runner (`packetstation migrate`) as the migrate role; 0001 creates roles and grants (app: insert+select on audit, no update/delete).
- compose.test.yml (app, worker, postgres, internal network, npm_default external with alias, secrets), compose.ci.yml (adds samba-ad-dc), healthchecks, logs to stdout.
- Gitea Actions: build image, run pytest, lint. Playwright comes in phase 1.
- Settings table and environment loading; X-Forwarded-* trust only from npm_app_1's resolved address, re-resolved on change; security headers and CSP; build version endpoint.
- Closes: H1, H2, H4, A4 (role), S1 (headers; CSP tuned again in phase 10). H3 is NPM's TLS plus the app's HSTS header, closed here.
### Phase 1: Identity, sessions, audit, frontend shell (medium)
- Tables: users (objectGUID key), sessions, devices, pins, signin_attempts, audit, settings. ldap3 ServerPool FIRST over LDAP_URL; search-then-bind; nested groups via LDAP_MATCHING_RULE_IN_CHAIN; disabled accounts refused; roles = union over groups; derived group-to-role map from the department table.
- Rate limits per account and per address before the bind; PIN rules (6 to 12 digits, Argon2id, 5 strikes disables, 3 wrong at unlock or signature ends the session); device registration (Admin or shop Supervisor, password session); PIN sign-in by employee ID on registered devices only; session kinds and timeouts as settings; Origin check; idle and background lock reported by the browser and enforced by the server; step-up unlock.
- Signatures table and the `sign` helper used by every sign-off action later.
- SSE endpoint `/api/events` (session required, X-Accel-Buffering: no) with LISTEN/NOTIFY fan-out and presence.
- Frontend: Vite + React + TS, PESCO dark theme port from app.src.html, sign-in (password and PIN keypad tabs), lock screen, role switcher, Account settings (AD details, PIN create/change with password, sessions, own access log), Admin shell with the read-only AD view, devices, PIN clearing, group map and missing-group flags.
- Samba AD DC container for CI, seeded with the 50 groups and test users (employeeID set); Playwright sign-in scenario runs in CI.
- Server tests: bind flow against the CI DC, rate limits, PIN lifecycle, session timeouts, Origin check.
- Closes: I4, I7, I8 (app part), S2; partly I1, I3.
### Phase 2: Core domain, policy engine, packet API (large)
- Schema for departments (seeded; code and slug immutable), stations, user profiles (stamp, station limits), projects, units (aliases, renumber history, soft delete), packets and child tables (see Schema section), packet_log, assets metadata.
- Policy port in Python: Action enum, custody, transition table, CO hold, field-edit rules, the UI-only preconditions now server-side, `can: [...]` on every packet read. Tests try every transition as every role in every status (the matrix), plus custody, short-parts and CO exceptions.
- Write pipeline: session, policy, transition, validation, version check, audit row, one transaction, NOTIFY. Compare-and-set on status and dept for workflow steps (409 "someone else already took this step"); row-level versions on child rows; append-only log.
- Admin edits with reason and IT-edit marking; filed lock; `cleanText` equivalents; input limits.
- Frontend: home layout (headline, KPIs, attention, find bar, boards or list), Clerical screen (boards, projects and units with two-step pickers, renumbering, move packet), packet page (header, tabs, history, drawing viewer with pdf.js, documents), hand-made draft creation, send to warehouse or department or Q.C.
- Closes: A1, A2, A3, M1, S5.
### Phase 3: VISUAL feed and drawings in the worker (large)
- Cache tables: visual_batches, visual_rows, visual_materials, visual_orders, visual_labor (if kept). Worker process with APScheduler, settings re-read each tick, shop-hours intervals, pause, job_requests + NOTIFY "run now", advisory lock per job, job_runs with results.
- Feed job: the five reads (released for the window firmed only, open packets' WOs by key, materials, order links, labor), batch diff, packet rules (new drafts, update drafts in place, retire moved drafts, never recreate removed, flag WO-gone, observe Closed, projects and units auto-created with customer, PO and dates from order links with provenance and Clerical override), go-live boundary with the ignored list and "pull in by hand", fingerprint function with tests that show which fields open a change order and which are minor, change orders opened or re-aimed on in-shop packets, auto-release to warehouse.
- Drawing job: queue (needed, recheck due, rev changed, CO lookups), budget, new DrawingLookup per tick, decision table (attach, rev mismatch, not released, not found, failed), H-sheet base rule, download with sha256 into assets, pages, hand drawing removed on new VISUAL rev, newer released rev on an in-shop packet opens a CO.
- Clerical UI: From VISUAL panel (run now, status, last ten runs, ignored list, not set up, projects without names), flag kinds, rev-mismatch approval by Clerical supervisor with signature and reason, hand upload of drawing and BOM on flagged drafts (server-side XLSX and CSV parse), change-order approve, reject, hold.
- Admin: Feed settings tab, resource map editor, department picker.
- Tests: packet rules against recorded DEV feed output (local only) and the synthetic generator (CI); fingerprint table; drawing decision table with a fake lookup.
- Integration run against VISUAL DEV and the production vault from the developer machine.
### Phase 4: Warehouse (medium)
- Task board (incoming, pulling, hold, pulled, released 3 days), hold reasons, pull table (pulled, issued, location, heat with scan, N/A, MTR upload), shorts with receipts, send back with reason, release to supervisor with the server-side gate.
- Upload pipeline: type by content, size limits, sanitised names, stored by id, Content-Disposition and nosniff, optional ClamAV service, volume quota alert.
- Closes: D5.
### Phase 5: Shop: supervisor and stall (large)
- Supervisor screen (KPIs, station grid, CO on hold, boards, assign station and welder with station limits, pull back, send back, review: send to Q.C. with signature, return to station, skip Q.C. route), stall screen (my packets, start, pause, resume, submit with signature, checklist, weld record fields).
- Weld map and heat table on the drawing (pdf.js + canvas, positions as fractions, server-assigned W<n> and H<n> ids), heat map borrowing from a sibling packet, timer with the server as the clock of record, ticket hours beside it (if kept), stages on routing.
- Change-order verification by department supervisors, VOID sheets, carried marks.
- Closes: S8 (print stamps come in phase 7, so S8 closes there).
### Phase 6: Q.C. (medium)
- Inspection tab (checks, visual exam per weld, NDE, hardness with NACE label, notes), accept with route and signature, reject with reason or NCR and signature, Q.C. project folders with superseding revisions, packet documents, digital ITP (template, lines, N/A, rejects with NCR number, block signatures, sign-off that files the ITP PDF), hold-point warnings by department page group, NCR validation hook reserved (pesco-ncr `GET /api/ncrs/{ref}` needs a token; later phase).
### Phase 7: Close, file, PDFs and prints (large)
- Clerical close: closed-in-VISUAL observed state, vclosed confirm, warning on filing with open WOs, mark filed with signature and HMAC seal, reopen by Admin with reason and new seal keeping the old.
- Server PDFs: data book (reportlab + pypdf + pypdfium2, page for page after databook.js: cover, index, S1 drawings with VOID stamps, S2 traceability and heat-map rasters, S3 welding records, S4 Q.C., S5 Q.C. docs, S6 MTRs, S7 history), traveler with QR codes, ITP PDF, traceability CSV with the formula guard, print kinds with "printed by" stamps and print logging, controlled-copy stamp signed with the seal key, metadata. Data book filed automatically when the packet is filed.
- Closes: M2, S6, S7, S8.
### Phase 8: Project Management, find bar completion, presence (medium)
- PM screen tabs (overview, projects, aging, hours, quality, find), periods, health, CSV and PDF exports logged.
- Find bar full search set, status and flag filters, list view sorts, filters saved per browser; presence in packets via SSE; "Mine" flag; motion with reduced-motion switch; iPad viewport layout pass.
### Phase 9: Admin completion, reconcile, notifications, backups (medium)
- Departments tab (display name, stations; add department shows the three group names), profiles with CSV import by employee ID, "refresh from AD now", nightly ad-reconcile, 15-minute role refresh, Security tab (settings, alerts), access log with export, integrity job (seals, asset hashes, log to audit links) with alert.
- Notifications tab (rules per event type, recipients: group, user, mailbox, dynamic department Supervisor group; immediate or digest; exclusions; test send; sent log; dry run on test), SMTP via app-03 with STARTTLS and CA verify, subject prefixes, seed to PS-Admins.
- scripts/backup.sh + systemd timer (America/Denver 02:00), 7 dumps, marker; worker backup-check job; restore script; restore rehearsed on the test stack and written up.
- Closes: I6 (app part), M3, O1, O2 (first rehearsal), S3 (pip-audit and npm audit in CI).
### Phase 10: Regression bench, hardening, go-live (medium)
- Rewrite the 38 scenarios against the new UI with synthetic fixtures; run in Gitea Actions against compose.ci.yml. Policy matrix, merge rules and packet-rule tests already run from phase 2 and 3.
- CSP final pass, dependency scan, build-version banner and reload, DEPLOY.md with the NPM proxy host settings, SECURITY.md rewritten with the ID table, GO-LIVE.md (date, boundary, groups, tablets, URL, M: retirement as read-only archive), PAPER-FALLBACK.md, incident notes.
- Production compose with explicit VISUAL_SERVER and VISUAL_DATABASE; first start is Spencer's approval.
- Closes: O3, O4; S4 is IT's pentest, deferred.
## SECURITY.md Part 3: which phase closes each ID
| ID | Status |
|---|---|
| H1, H2 | Closed, phase 0 (docker-1, internal DNS, NPM) |
| H3 | Closed, phase 0 (NPM TLS, HSTS from the app) |
| H4 | Closed, phase 0 (two stacks) |
| H5 | IT's (host patching); documented in DEPLOY.md |
| I1 | Partly closed, phase 1 (AD sign-in, no SSO); Entra SSO deferred |
| I2 | Deferred (MFA with Entra) |
| I3 | Closed for "PIN on the server", phase 1; device certificate deferred |
| I4 | Closed, phase 1 |
| I5 | Superseded (no in-app admin grant); separate IT admin accounts remain IT's |
| I6 | Largely closed, phase 9 (groups plus reconcile); quarterly review IT's |
| I7 | Closed, phase 1 |
| I8 | App part closed, phase 1; kiosk lock policy IT's (Intune or GPO) |
| A1, A2, A3 | Closed, phase 2 |
| A4 | Closed, phase 0 |
| D1, D2, D4 | Deferred (envelope encryption, Key Vault) |
| D3 | IT's (host disk encryption on docker-1) |
| D5 | Closed, phase 4 (optional ClamAV) |
| D6, D7 | Deferred (retention period and customer copy need a decision from Seth and Q.C.) |
| M1 | Closed, phase 2 |
| M2 | Closed, phase 7 |
| 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 |
| S6, S7, S8 | Closed, phase 7 |
| 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) |
## Schema (phase 1 to 3 migrations; full DDL goes in docs/SCHEMA.md)
Conventions: identity bigints except users (uuid = AD objectGUID); Postgres enums for packet_status (declaration order = workflow order), wh_stage, co_review, role_kind, session_kind, sig_factor; every timestamp timestamptz; every "who" column references users; rows backed by a signature carry signature_id. Soft delete = removed_at, removed_by, removed_reason with partial unique indexes `WHERE removed_at IS NULL`. Append-only tables get INSERT and SELECT for the app role plus a BEFORE UPDATE OR DELETE trigger that raises.
- **Identity:** users (id uuid, sam_account, upn, display_name, emp_no unique, email, welder_stamp, active, directory_flags, ad_synced_at); user_roles (user_id, role, dept_code, synced_at; rebuilt at sign-in and by the reconcile); sessions (token_hash, kind password|pin, device_id, expires_at, last_seen_at, locked_since, revoked_at, ip, user_agent, roles_snapshot jsonb); devices (name, token_hash, registered_by, revoked_at); user_pins (argon2id hash, failed_count, disabled_at); signin_attempts (append-only: account, ip, kind, ok, reason; drives both rate limits); signatures (append-only: user, account and name snapshots, session kind, factor, action, meaning, packet_id, subject, content_hash).
- **Organisation:** departments (code PK, slug unique and immutable by trigger, name, kind, sort, active, weld bool); stations (dept_code, name, sort, active); user_station_limits (user_id, station_id).
- **Projects and units:** projects (number, name, customer, po, due, status, notes, source, needs_details, version, order-derived columns with `*_src` and `*_visual` companions); project_aliases (alias PK, project_id, kind); project_renumbers (append-only); units (project_id, lot char(3), own due and details, same companions). Unit number is a view column `number || lot`.
- **Packets:** packets header (unit_id, dept_code, op, title, customer, drawing, rev, wps, status, status_at, station_id, welder_id and stamp snapshot, for_sup_id, heat_map nullable, wh_skipped, wh_sent_at, routed_to, return_note, vclosed_at, databook_at, source_kind visual|hand, source jsonb, visual_key unique partial, visual_rel, visual_want, drawing_state, flags text[], visual_fp_applied, visual_closed_at, visual jsonb {folded, partspecs, vault, appr, revOk, manualDwg, manualBom, moved}, next_weld_no, next_heat_no, next_page_no, next_co_no, version, timestamps, soft delete). Children: packet_work_orders (unique on wo key, visual_status, missing_runs, gone_at), packet_pages (n, type, asset_id, pn, co_id, void_*), bom_lines (unique item, heat, na, issued, loc, pulled, mtr_asset_id, src, vkey, whs, comm, cons, sub, grp, row_version), bom_shorts (one open per line), short_receipts (append-only), welds (weld_no, page, fractions, wo, piece, items, type, size, process, stamp, welder_id, stage_id, carried), heat_tags (legacy tag_no, page, item, fractions, carried), heat_table_placements (page, x, y, s), packet_stages (one open row; checklist jsonb, filler, filler_lot, preheat, notes, clock columns, opened and closed), packet_wh (stage, by, at, reason, note, auto), packet_transitions (append-only: action, from and to status and dept, reason, signature_id; replaces hist and sentBack), qc_inspections (one open; checks jsonb, weld_exam jsonb, nde_*, hardness, notes, book, spool, location, result), qc_history (append-only with snapshot jsonb), packet_seals (HMAC hash, key_id, broken_*; one unbroken per packet), packet_log (append-only, user-visible history with it_reason), packet_visual_state (observed per-leg status, ticket hours, order).
- **Change orders:** change_orders (co_no, origin visual|drawing|manual, review pending|approved|rejected, minor, hold, held_by, issued_*, approved_*, rejected_*, vis jsonb, waiting jsonb, staged jsonb, target_fp; one pending visual CO per packet), co_items, co_required_depts, co_verifications (insert-only, signature required). View v_open_cos ports openCOs.
- **Documents:** assets (uuid, sha256, size, mime, original_name, storage_key, source, source_ref jsonb, uploaded_by, packet_id); packet_docs; qc_docs (project, unit, type, rev, auto, superseded_by); itps (unit unique, template jsonb frozen, status, signoff_*, reopen_*), itp_lines, itp_rejects (append-only, ncr), itp_blocks.
- **Audit, settings, jobs:** audit_events (append-only, prev_hash and row_hash chain, user, session, role_snapshot, action, subject, packet_id, detail jsonb, it_reason, ip); settings (key, value jsonb, version); job_requests, job_runs (trigger, requested_by, status ok|degraded|failed|skipped|abandoned, counts jsonb, errors jsonb, build_version, visual_target), job_schedule, worker_heartbeat.
- **Notifications (per the brief):** notification_rules (event_type, enabled, delivery immediate|digest, digest_time); notification_recipients (rule_id, kind group|user|mailbox|dept_supervisor, group_dn, user_id, address); notification_exclusions (rule_id, user_id); notification_outbox; notifications_sent (event, recipients as resolved, subject, result); notification_state (dedupe and recovery).
- **VISUAL and drawing caches:** visual_batches; visual_rows (typed key wo, lot, split, sub, seq int; all contract fields; row_hash; first, last and gone batch); visual_materials (plus piece int, mat_hash); visual_orders and visual_order_links (base or project, rank, lines, top_jobs, order_date); visual_labor; visual_row_changes and visual_material_changes (per batch, added|changed|gone); visual_ignored_wos (go-live list with pulled_in_*); visual_resource_map; drawing_lookups (dwg PK, checked_at, ok, rev, name, object and file ids and versions, base, draft_rev, draft_where, in_drafting, note, failing_since, asset_id, visual_rev_seen).
- **JSONB only here:** foreign-shaped feed payloads (source, visual, partspecs, vis, waiting, staged, raw), template-driven answers (checklist, checks, itp template, settings), and frozen snapshots (roles_snapshot, qc_history, audit detail, job counts). Everything the policy or the boards filter on is a real column.
- **Versioning:** packets.version goes up in every transaction that touches the packet or a child; it is the ETag and the NOTIFY payload. Child rows carry row_version. Every write takes `SELECT ... FOR UPDATE` on the packet first, which replaces the old 8-second lease.
## Policy port and concurrency rules (phase 2; docs/POLICY.md)
- **Inputs:** `Principal` (user, admin, support set, clerical_supervisor, shop dept to level map, station limits, session kind, or `system="visual-feed"`) and `PacketState` (status, dept, station, wh_skipped, routed_to, vclosed, removed, open COs, open shorts). Pure functions in `server/packetstation/policy/`, no I/O.
- **Custody** `holds()` is a direct port: draft CL; warehouse WH; ready and review dept lead or above; assigned and in_progress dept lead or above or works_on (worker in dept whose station limits include the station, and, tightened per the README, the assigned welder for start and submit); qc QC; complete CL; filed nobody.
- **Action enum** with a rule per action: who, from and to status, dept change, CO hold, preconditions, signature meaning, reason required. Transitions: SEND_TO_WAREHOUSE, RELEASE_TO_SHOP, SEND_DRAFT_TO_QC, RETURN_TO_CLERICAL, WH_RELEASE, WH_SEND_BACK, SEND_BACK_TO_WAREHOUSE, ASSIGN, UNASSIGN, PULL_BACK, START, SUBMIT, RETURN_TO_STATION, SEND_TO_QC, ROUTE_FROM_REVIEW, QC_REJECT, QC_ACCEPT, QC_ACCEPT_AND_ROUTE, FILE, ADMIN_REOPEN. Edits: EDIT_BOM, WH_RECEIVE_SHORT (a narrow endpoint that replaces onlyShortLines), EDIT_HEATS, EDIT_WELDS, EDIT_CHECKLIST, EDIT_STAGE_FIELDS, CLOCK, EDIT_QC, SET_WH_STAGE, EDIT_DEPT, MOVE_UNIT, EDIT_WOS, EDIT_DOCUMENT_SET, EDIT_HEADER, REV_APPROVE (clerical supervisor), SET_VCLOSED, SET_DATABOOK_AT, DOC_ADD, DOC_REMOVE, CO_ISSUE, CO_APPROVE, CO_REJECT, CO_HOLD_TOGGLE, CO_VERIFY, ITP_*, VIS_* (worker only).
- **Preconditions** as named predicates, now server-side: heat_map_answered, drawing_attached, bom_all_pulled_or_short, welder_set, checklist_complete, welds_recorded, filler_set, qc_book_spool_location, qc_missing_empty, reason_given, vclosed_if_databook (warning only when work orders are still open in VISUAL; block only when vclosed is unset and the route is Data Book), co_staged_if_dwg_change.
- **Order of checks** keeps the old semantics: removed, wrong status, CO hold (applies to admins too), admin bypass of custody with a required IT reason, filed lock (only SET_DATABOOK_AT and ADMIN_REOPEN), who and custody, then preconditions (never bypassed).
- **`can` on reads:** every packet read returns `can: [...]` (passes role, custody, status and preconditions), `blocked: {action: [reasons]}` (passes role and custody but fails a precondition or the CO hold, so the UI can show a disabled control with the reason), `holder`. Boards evaluate transitions only from header columns plus an open-CO count.
- **Signatures:** required for SUBMIT, SEND_TO_QC, QC_ACCEPT, QC_ACCEPT_AND_ROUTE, QC_REJECT, FILE, ROUTE_FROM_REVIEW, ADMIN_REOPEN, CO_ISSUE, CO_VERIFY, CO_APPROVE, CO_REJECT, REV_APPROVE, hand uploads, ITP_BLOCK_SIGN, ITP_SIGN_OFF. The body carries `sign: {factor, secret}`; the server verifies the factor against the session's own user inside the transaction (PIN allowed in any session on any device, as the brief says; password everywhere), logs the attempt, applies the five-strike PIN disable and the three-strike session end, and writes the signatures row with a content hash over what was signed.
- **Concurrency per table:** (a) transitions are compare-and-set on `expect: {status, dept}` under the row lock, 409 "someone else already took this step" with the current state, no packet-version check so a weld edit never blocks a send; side effects (open or close stage, qc_inspection, wh reset, seal) run in the same transaction. (b) keyed rows: bom_lines and qc_inspections PATCH with If-Match row_version; welds, heat tags and pages get server-assigned numbers from the packet counters so concurrent adds never collide; shorts and CO actions are state CAS; co_verifications are inserts (PK violation = already verified); checklist keys use jsonb_set per key; heat table position is an idempotent upsert; work orders are a draft-only full replace with If-Match packet version. (c) append-only tables never conflict. (d) header scalars use field-level CAS (`set` plus `base`), the old merge3 made explicit; databook_at, clock start and stop, and system-written visual sub-keys need no check.
- **API shape:** resource endpoints under `/api/packets/{id}` (transitions, bom, bom/{item}/short, welds, heat-tags, heat-table, stage, stage/checklist/{key}, stage/clock, wh, qc, work-orders, pages, docs, change-orders/{no}/verify|approve|reject|hold, seal, reopen, log), plus /projects, /units (+renumber, itp), /qc-docs, /assets, /settings, /admin/*, /account/*, /events (SSE). Every mutating response returns the new packet version; every 409 returns the current state.
- **Worker writes** use the same helpers with `Principal(system="visual-feed")`: VIS_UPDATE_DRAFT, VIS_OPEN_CO, VIS_AUTO_RELEASE (same preconditions as SEND_TO_WAREHOUSE plus rev confirmed and no flags), VIS_RETIRE_DRAFT. It never writes shop-side tables; in-shop packets only get a change order.
## Worker, feed and drawings (phase 3; docs/FEED.md)
- **Process:** `packetstation worker` runs APScheduler (BlockingScheduler, 4 threads, coalesce, max_instances 1) in site time for scheduling, UTC for storage. One psycopg pool as the worker role, one LISTEN connection, one more connection per run holding `pg_try_advisory_lock(7310, hashtext(job))`. One `Visual.from_env()` per run in the job thread, with a stack guard (test stack refuses a production target; production stack refuses an implicit DEV target). One MFiles Vault per process, a new DrawingLookup per run. Heartbeat row and file for the healthcheck.
- **Jobs:** visual-feed (in-shop and out-of-shop intervals from settings), drawing-lookup (chained after every feed run, plus its own cadence), session-refresh (15 min), ad-reconcile (nightly 01:30 site time plus "refresh from AD now"), integrity (04:00), backup-check (07:15), notify-send (60 s), notify-digest (daily at the digest time), housekeeping (prune caches at 90 days, job_runs at 180). A `SettingsTrigger` reads settings on every fire; the app NOTIFYs `ps_settings` on save so a new interval or pause applies at once. Pause stops scheduled feed and drawing runs only. `job_schedule` holds the next run for the Admin tab.
- **Run now:** app inserts job_requests and NOTIFYs `ps_job_request` in the same transaction; the listener reschedules the job to now; the run claims all pending requests so ten clicks make one run; a request during a run fires another run after.
- **job_runs:** trigger, requesters, started, finished, status ok|degraded|failed|skipped|abandoned, counts (wos_window, wos_tracked, rows, materials, orders, labor_tickets, new_drafts, drafts_updated, cos_opened, cos_reaimed, cos_withdrawn, retired, wo_gone_flagged, closed_observed, ignored_golive, units_created, auto_released, unmapped_resources; for drawings checked, fetched, unchanged, attached, flagged by kind, lookup_failed, cos_opened, budget_left), errors redacted and truncated.
- **Feed reads (outside any transaction):** window from site-local today with `until` always passed; (1) `released()` firmed only; (2) `get_many` for every work order key on an unfiled packet plus pulled-in ignored ones, no status or date filter, missing keys collected; (3) operations, partspecs, parts and materials for the union, with `mat_known` per leg so a closed leg does not look like a removed BOM; (4) order links by base and by project, degraded on failure with last cache kept; (5) labor tickets at most every 30 minutes when the setting is on. A suspect batch (zero window rows after a full one, or missing keys above 10% and 5) is applied without the "gone" step and marked degraded.
- **Cache:** COPY into temp tables, MERGE into visual_rows and visual_materials keyed on typed fields, change rows per batch for the Admin "what VISUAL changed" view. The apply recomputes candidates for everything in play (cheap) and writes only where fingerprints or state differ.
- **Apply (one transaction, lock_timeout 5 s, FOR UPDATE SKIP LOCKED with retry next run):** candidates per the old rules (fold and ignore, unmapped counted, project from the base prefix, unit = project + 3-digit lot, visual_key unit|res|dwg, legs deduped, rel earliest, want latest, rev highest numeric, folded, partspecs, BOM best status per piece with group headers and sub/na rules, existing packets keep their tracked legs whatever the date); go-live boundary writes visual_ignored_wos unless pulled in; match on visual_key across live and removed packets; new → draft with fingerprint; removed → nothing, ever; draft → update in place (BOM unless hand, drawing removed and flags cleared when dwg or rev moved, hand drawing taken off); past draft → fingerprint compare, open or re-aim the single pending visual CO, withdraw it if VISUAL reverted, follow an approved one with a new CO; minor = only tgt or want changed, needs Clerical approval only; retire untouched drafts whose legs all moved (soft-removed with reason "moved", visual.moved set, hidden from boards); work order gone flagged after 2 consecutive misses, cleared on return, never removed; closed observed per leg and on the packet when all legs are C, cancelled flagged; projects and units auto-created with order-derived customer, PO, order and ship date carrying `*_src` provenance, Clerical override with reason never overwritten; auto-release up to 25 with heat_map defaulted; one audit row per packet change plus a run summary; NOTIFY on commit.
- **Fingerprint:** `compute(candidate) -> (full, parts)` with canonical JSON (NFC, LF, trimmed, null and "" alike, IDs upper-cased, lot/split/sub text, seq and piece int, Decimal-normalised numbers, ISO dates, sorted lists). Parts: legs, qty, rev, drawing, bom (major) and tgt, want (minor). Never in it: rel, folded, partspecs, title, status, issued, whs, loc, orders, labor. A parametrised test per field shows whether a change opens a CO and whether it is minor, plus invariance tests (2 vs 2.0, "" vs null, order, m00543 vs M00543, lot "7A").
- **Drawing job:** queue over unfiled, unretired packets with a drawing and no hand drawing (unless the rev changed), distinct by dwg, ordered: rev changed or CO target rev; drafts needing a drawing; failed lookups with backoff; rechecks due. Budget 60 per run. Download only when object id and version or file id and version changed; PDF magic and size check, streamed sha256, fsync and rename into assets, dedupe by sha256 plus source_ref, page count via pypdfium2. Decision table: lookup failed → flag and keep what is attached; not released → flag with drafting note; not found → flag "check the drawing number in VISUAL"; non-integer VISUAL rev → flag, not attached; attach → attach on drafts (H sheet noted), on in-shop packets nothing when same rev and file, a drawing-origin CO when the same rev has a new file with a different hash; rev mismatch → flag and stage the M-Files copy for Clerical supervisor approval with signature and reason; newer released rev than the attached one on an in-shop packet → drawing-origin CO, never a silent swap; a feed CO with a rev change gets its staged drawing from the lookup of the target rev. "Drawing needed" blocks the warehouse step through the policy precondition.
- **Other jobs:** ad-reconcile (list the OU, report missing and extra groups, enumerate members with the IN_CHAIN filter paged at 500, upsert users on objectGUID, mark disabled or gone users inactive and end their sessions, flag missing or duplicate employeeID); session-refresh (re-resolve groups and userAccountControl per live user); integrity (seals, asset hashes 7 days nightly and full on Sundays, missing and orphan files, audit hash chain); backup-check (marker and newest dump under 26 h); notify-send and notify-digest (recipients resolved at send time, STARTTLS verified, dry run on test, one digest per recipient).
- **Several open orders:** proposed rule (b) unit match on `top_jobs` (unit number is the top-job name), fall back to open-first-newest, plus (c) a Clerical pin per project or unit with reason; the project page lists every open order. Open question 5.
## Deployment and CI (phase 0, 9, 10; docs/DEPLOY.md)
- **Dockerfile:** frontend stage on node:22-bookworm-slim; builder on python:3.12-slim-bookworm with a venv, `pip install --require-hashes --index-url <gitea>/simple/ -r server/requirements.lock` then the package; runtime with the Microsoft Debian 12 repo (msodbcsql18, unixodbc, tini, tzdata), non-root uid 10001, `PS_BUILD` label. entrypoint exports MFILES_PASSWORD from the secret file and dispatches app (uvicorn with no proxy headers; the app's TrustedProxyMiddleware re-resolves npm_app_1 every 60 s), worker, migrate, doctor, job run, fixtures.
- **Compose:** base file with postgres (16-bookworm by digest, `backend` internal network only, initdb creates ps_migrate, ps_app and ps_worker from secrets), one-shot migrate, app and worker (backend plus egress, assets volume, CA bundle bind, secrets, healthchecks, restart unless-stopped, json-file logs, 1 GB limits; worker also mounts the backup folder read-only). Proxy overlay joins app to npm_default with `APP_ALIAS`. CI overlay adds the Samba DC, fixture sources and dry-run mail. Env files per stack in /etc/packetstation; secrets root:10001 0440. Deploy: `docker compose -p packetstation --env-file ... -f compose.yaml -f compose.proxy.yaml up -d`.
- **NPM proxy host** (recorded in deploy/npm-proxy-host.md): http to the alias on 8000, Force SSL, HTTP/2, NPM HSTS off (the app sends it), `client_max_body_size 30m`, a `location /api/events` block with buffering off and 3600 s timeouts using the `$server` variables so the alias is re-resolved.
- **Doctor:** config and stack guard, `visualkit doctor`, `mfileskit doctor`, DB connect and migrations at head and an audit UPDATE that must fail, LISTEN/NOTIFY round trip, assets volume and free space, backup folder, LDAP TLS bind reading memberOf, employeeID and userAccountControl with the 50-group count and one IN_CHAIN query on both controllers, SMTP STARTTLS verify, tzdata.
- **Backups:** scripts/backup/ps-backup.sh per stack via `docker exec pg_dump -Fc` plus globals, verified with `pg_restore --list`, 7 kept, last-success and last-failure markers in /var/backups/packetstation/<stack>/; systemd timer `OnCalendar=*-*-* 02:00:00 America/Denver`; restore script and rehearsal recorded in DEPLOY.md.
- **Gitea Actions:** ci.yml with server (ruff, mypy, pytest against a postgres service: migrations from zero, role privileges, policy matrix, fingerprint, rules on generated feeds, scheduling, drawing decision table), frontend (lint, tsc, vitest, build), image (buildx, push `:sha-<sha>`), e2e (compose up with the CI overlay, fixture seed, Playwright scenarios switching fixture states through `packetstation fixtures use`, report upload, down -v). release.yml re-tags the tested sha image as `vX.Y.Z`; never latest.
- **Samba AD DC for CI:** built in the repo from debian bookworm with samba, provisioned as PSTEST.LOCAL with its generated CA exported for LDAP_CA_BUNDLE so the TLS path is real; seed.sh creates the OU tree, the 50 groups from `packetstation ad-groups --list` (one deliberately missing and one extra to test the report), and test users per role plus multi-role, nested, disabled, no-employeeID, duplicate-employeeID and no-role users with employeeID set by ldbmodify.
## Open questions
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. 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. The device cookie stays and no device certificate is planned: confirmed by Spencer (IT) on 2026-10-18, Gitea issue 3.
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.
6. **VISUAL_ENCRYPT** (IT, before production): yes once vsql has a trusted certificate; `no` until then.
7. **Policy tightenings** listed under "Challenges" 1 to 3 (Seth, before phase 2).
8. **Repo location**: PacketStation2 as proposed, or another name.
9. **Retention period** (D6) and customer-facing data book copy (D7): deferred, but a decision from Seth and Q.C. lets a later phase close them.
Smaller questions from the design, with the default the build takes unless told otherwise:
10. Same-rev drawing re-release (new file, same rev) on an in-shop packet: default a drawing-origin change order, not a silent swap.
11. Cancelled (X) work orders: default flag only, like gone; drafts are not removed.
12. Partspecs, title or folded-operation target changes on an in-shop packet: default no change order, as the old build.
13. Work-order-gone confirmation after 2 consecutive runs and the 10% suspect-batch guard: defaults as stated.
14. Pause scope: default pauses the feed, the drawing job and auto-release together.
15. Gitea runner: whether the Samba DC container can run there (overlayfs xattrs, possibly cap SYS_ADMIN). Checked in phase 1; if not, CI uses a runner with a Docker socket on docker-1. Phase 1 finding (2026-10-16): the container needs `cap_add: [SYS_ADMIN]` (Samba 4.17 sets sysvol NT ACLs through `security.*` xattrs) and runs that way on docker-1; the Gitea runner itself is still to be tried. Phase 2 (2026-10-16): an act_runner with the host Docker socket is registered on docker-1 as `docker-1` (deploy/runner/); resolved: the runner passes `cap_add` through, and the e2e job (stack with the DC plus the Playwright scenarios) passed on it on 2026-10-16.
16. NPM: confirm `Block common exploits` does not interfere with uploads; checked when the test proxy host is made in phase 0.
17. SET_DATABOOK_AT is writable by anyone in the old build, even when filed: default restrict to Clerical and Q.C.
18. CO verifications cannot be withdrawn; ITP line signatures can be undone by the same inspector or an Admin before the block is signed.
From phase 4 (2026-10-16), for Seth to veto:
19. The release gate has two server rules: every line not N/A is pulled or on an open short, and every pulled line that issued pieces has a heat unless it is N/A or a sub-assembly. The old build enforced both on the screen only. Default: both on the server.
20. Hold and short reasons: the old build accepted any text. Default: closed sets on the server; "Other" on a hold needs a note.
21. Receiving a short on a packet the warehouse no longer holds: default it stays a warehouse right (WH_RECEIVE_SHORT, any status but filed), and the holder of the packet (department lead or supervisor, Q.C., Clerical) can receive too, through edit_bom.
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). Confirmed by Spencer (IT) on 2026-10-18, Gitea issue 19: the limits stay, and ClamAV stays off in both stacks because every workstation that uses Packet Station runs Microsoft Defender. The `clamav` profile stays in the build, unused.
23. The quota alert: default a `notification_events` row (phase 9 builds the delivery on this table) at 85 percent of the assets volume. Confirmed by Spencer (IT) on 2026-10-18, Gitea issue 20: 85 percent stays, and the alert goes to PS-Admins. Phase 9 seeds the `assets_quota` notification rule with PS-Admins as its recipient.
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.
From phase 6 (2026-10-17), for Seth and Q.C. to veto:
27. Q.C. reject: the old build took any non-empty reason. Default: a reason of at least 8 characters, or a well-formed NCR number (`NCR-YYYY-NNNN`) in its place, or both. The NCR is stored on the inspection and linked to pesco-ncr. Applied in phase 6.
28. A filed packet in a unit and the unit's ITP: the ITP belongs to the unit, not to a packet, so ITP writes never look at a packet's status. Default: a filed packet does not lock the ITP. This resolves the phase 5 follow-up. Applied in phase 6.
29. ITP status: the 0003 enum had `open`, `signed` and `reopened`. Default: `itp_status` gains `complete` for a signed-off ITP; a reopened ITP is `open` again with the reopen recorded. `signed` and `reopened` stay in the enum unused, since PostgreSQL cannot drop an enum value. Applied in phase 6.
30. Removing an ITP block signature: the old build let any Q.C. editor undo it. Default: the signer or an admin, while the ITP is open. The block's QN, remarks and customer fields stay. Applied in phase 6.
31. The signed ITP PDF on sign-off: default a hook (`pdf.itp.file_signed_itp_pdf`) that phase 7 builds. Today it files nothing, the sign-off log says `PDF filing reserved for phase 7`, and the automatic `qc_docs` ITP row is written only when the hook returns a file. Applied in phase 6. 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.
From phase 8 (2026-10-18), for Seth to veto. The phase 8 design numbered these 35 to 39; 35 to 37 were already taken by phase 7, so they follow on from 38.
38. Hours vs target: the station timer stays the clock of record. "Over target" means timer hours over the work-order target. VISUAL ticket hours are shown beside the timer in every hours table and never decide the flag. Phase 8 (2026-10-18) applied the default.
39. PM exports and who reads the reports: every PM export is logged as `report exported` in the audit and in `print_log`, with the scope and the period. Default: any signed-in role may read the PM reports (as every role may open every packet), not only PS-ProjectMgmt. Phase 8 (2026-10-18) applied the default.
40. Presence shows the display name and the role (the screen the person has open). Default: locked sessions drop out of presence at once; closing the tab drops out within the stream's ping cycle (25 seconds). Phase 8 (2026-10-18) applied the default.
41. The find bar filters are applied by the server on every board and list, so a board never fetches more than it shows. Default: the KPI numbers follow the project pick only, not the other filters. Phase 8 (2026-10-18) applied the default.
42. Mine is decided from the person's AD roles and the screen they are on. Default: the stall's Mine needs the picked station and shows nothing without one. Phase 8 (2026-10-18) 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: 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). Done in phase 8 (2026-10-18); the page table, the fixes and what is left are in the phase 8 comparison note.
- 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.
## Host layout on docker-1
OPS2 (Gitea issue 38): the test stack's files stay under `/home/spencerm/packetstation/test`, like the other compose stacks on docker-1 (gitea, npm, pesco-ncr and the rest live under `/home/spencerm`). Decided by Spencer (IT) on 2026-10-18. The move to `/etc/packetstation/test` is dropped. Whether the production stack follows the same layout is asked on issue 38. Until it is answered, DEPLOY.md keeps `/etc/packetstation/prod`.