Phase 8: Project Management, find bar, presence and motion
ci / frontend (push) Successful in 43s
ci / server (push) Successful in 11m6s
ci / image (push) Successful in 38s
ci / e2e (push) Successful in 3m55s

The Project Management screen (/pm, view only) with Overview, Projects & units, Bottlenecks & aging, Hours vs target,
Quality & issues and Find jobs, the period picker, health per unit and per project, drill down, Excel (CSV) per table
and for all, and the PESCO production status report PDF with the controlled-copy stamp; every export logged as
"report exported". The find bar on every home screen: the full search set, the dropdowns, quick flags with Mine, the
flag kinds, Board and List with sorts, filters saved per browser; every filter applied by the server on the list and
the boards, with facets. Presence in packets with role, lock and revoke drop; the motion switch with reduced motion
honoured, card glides, glow and count-up; Back restores the scroll spot; sheet placement and the iPad layout pass. The
data book compared page by page with the old build and six layout differences fixed. Tests for the PM aggregates,
search, filters, presence and exports; Playwright scenarios for the PM screen, the find bar and presence at desktop and
iPad viewports. Docs updated; PLAN.md questions 38 to 42.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-10-18 15:08:34 -06:00
co-authored by Claude Fable 5.1
parent 904437153d
commit 6b6db0230f
90 changed files with 10450 additions and 563 deletions
+9 -1
View File
@@ -268,11 +268,19 @@ From phase 7 (2026-10-17), for Seth and Clerical to veto:
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).
- 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.
+155 -1
View File
@@ -415,7 +415,7 @@ The sibling is a live packet in the same unit, with a different id and the same
### 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.
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. Phase 8 adds the role and the time the person arrived, and drops locked sessions (see "Phase 8: Project Management, find bar, presence").
### History texts
@@ -682,6 +682,160 @@ The `action` and `note` of `packet_log` for close, file and reopen.
The audit gains `print` (detail `{kind, what, pages, stamp_sig}`, plus `sheet` and `asset_id` when given) and `export` (detail `{kind, what, pages, stamp_sig}`, kind `databook`, `traveler`, `csv` or `itp`). FILE, ADMIN_REOPEN and SET_VCLOSED write their audit rows through the write pipeline as before.
## Phase 8: Project Management, find bar, presence
Nothing in the rule table changes in phase 8. Project Management, the find bar filters and presence add no policy action and no write.
### Who reads the Project Management endpoints
Every endpoint under `/api/pm` needs a signed-in session. Any role may read them, not only PS-ProjectMgmt, as every role may open every packet (open question 39). An unsigned request is 401. PM writes nothing: there is no POST, PUT, PATCH or DELETE under `/api/pm`, and the PM role passes no write action anywhere else. A packet opened from the PM screen shows "View only." The reports read PostgreSQL only. They never read VISUAL or M-Files.
| Endpoint | What |
|---|---|
| `GET /api/pm/overview` | The scope, the KPI tiles, the summary lines, the pipeline by step, and the units late or at risk |
| `GET /api/pm/projects?expand=all\|<project ids>` | Projects and units, with the units of the opened projects, or every unit with `expand=all` |
| `GET /api/pm/aging` | The pipeline and the three aging tables |
| `GET /api/pm/hours` | The four hours tables |
| `GET /api/pm/quality` | The five quality tables |
| `GET /api/pm/find?sort=` | The cards in scope, sorted |
| `GET /api/pm/export.csv?table=<key>\|all` | One table, or every table, as CSV. An unknown key is 404 `not_found`. |
| `GET /api/pm/report.pdf` | The production status report, inline unless `download=1` |
### The scope
Every PM endpoint takes the scope params: `project_id`, `unit_id`, `dept`, `status`, `station_id`, `welder_id`, `flags`, `flag`, `q` and `period`. They mean what they mean on `GET /api/packets` (below). `period` is `30`, `90`, `365` or `all`, default `90`. It limits the history figures only: Q.C. rejects, filed, lead time, sent back and the step history. `mine` in `flags` is ignored on PM.
- Projects in scope: every project not removed. A closed project only when a packet in scope belongs to it.
- Units in scope: the project's units. When any filter other than the project is set, only the units with a packet in scope.
- Health follows the old healthOf. `empty` with no packets. `done` when every packet is filed. `late` past the due date. `risk` when due within 7 days and under 75% done, or when any packet has an open change order or a warehouse hold. Else `ok`. A unit uses its own due date, else the project's. A project is as healthy as its worst unit.
- Over target means station timer hours over the work-order target hours. VISUAL ticket hours are shown beside the timer and never decide the flag (open question 38).
### Search
`q` on `GET /api/packets`, the boards and PM. Up to 8 words, and every word must match. A word matches when any field below contains it, case insensitive. A word over 60 characters is cut. The words `lot` and `rev` on their own match everything, so "lot 52" and "rev B" work as they did in the old build.
- The project: number, name, customer, PO and aliases.
- The unit: the unit number (`L670` and the lot) and the lot, both also with the lot's leading zeros stripped, so `52` finds lot `052`.
- The packet: op, title, drawing, rev, WPS, customer, the department name, the status label and the flag kinds. The word `flagged` matches any flagged draft.
- The station name, and the assigned welder's display name and stamp.
- The work orders: the base, and the base with the lot, split and sub (`L489052/1`).
- The BOM: heat, part ID, description and material.
- Change orders: `CO-<n>`, the bare number and the ECN.
- Welds: the stamp and the welder's display name.
A child match never repeats a packet. IDs are matched as text, never cast.
### Filters
The server applies the filters on every list and board (open question 41). A board never fetches more than it shows. A filter only narrows what the person could already read. It never widens access, and each board keeps its own scope checks (a supervisor's departments, the stall's station).
`status` takes one of the words below, or a comma list of raw statuses as before (`status=ready,review`).
| value | label | packets |
|---|---|---|
| draft | Ready to release | draft |
| warehouse | With warehouse (any stage) | warehouse |
| ready | Ready to assign | ready |
| shop | In the shop (ready to Q.C.) | ready, assigned, in_progress, review, qc |
| station | At a station (assigned / working) | assigned, in_progress |
| review | Supervisor review | review |
| qc | At Q.C. | qc |
| complete | Accepted / routed | complete |
| filed | Filed | filed |
| open | Everything not filed | every status but filed |
The other filters: `dept` (department code), `station_id`, `welder_id` (the assigned welder), `project_id` and `unit_id` (the project and unit pick), `flag` (the flag kind, below) and `flags` (the quick flags, below). The filters are joined with AND.
### Quick flags and Mine
`flags` is a comma list.
| value | chip | packets |
|---|---|---|
| co | Change order open | an open change order: pending, or approved with verifications still pending |
| short | Shorts open | at least one open short |
| overdue | Project overdue | the effective due date is before today |
| week | Due within 7 days | the effective due date is today to today + 7, or overdue |
| mine | Mine | the screen's own work, below |
Today is the date in the site time zone (`site_tz`, America/Denver by default).
Mine is decided on the server from the person's AD roles and the screen they name (`mine_screen`), never from anything the browser keeps (open question 42). Without `mine_screen` it is the first screen the roles give. A board defaults it to its own screen.
| Screen | Mine |
|---|---|
| clerical | status draft, or status complete routed to the data book |
| warehouse | status warehouse, or an open short |
| supervisor | the departments where the person is lead or supervisor |
| stall | the person's shop departments and the picked station (`mine_station_id`). No station picked: nothing. |
| qc | status qc |
| pm, or admin with no other role | everything. The chip is hidden on the PM and Stall screens. |
### Flag kinds
`flag` applies to drafts only, as the flagged rule does today. The raw values (`1`, `0`, `hand_upload`, a raw kind like `rev_mismatch`) keep working.
| value | label | rule |
|---|---|---|
| any | Flagged (any reason) | any flag |
| rev | Rev mismatch with M-Files | flag kind rev_mismatch |
| unconf | Rev not confirmed yet | a feed draft with its drawing attached (`drawing_state` attached) and `visual.rev_confirmed` not true: the rev approval is still pending |
| hand | Made or uploaded by hand | a hand packet, or flag hand_drawing or hand_bom |
| none | Still in drafting | flag kind not_released |
| vault | Drawing not found in M-Files | flag kind not_found |
| read | Drawing couldn't be read | flag kinds lookup_failed or bad_rev |
| wait | Drawing being looked up | flag kind drawing_needed |
| no | Not flagged | no flag |
### Sorts
`sort` on `GET /api/packets` and `GET /api/pm/find`.
| value | label | order |
|---|---|---|
| updated | Last updated | newest change first (the default) |
| project | Project / lot | project number, then lot, then op, in text order |
| due | Due date | the effective due date, earliest first, none last |
| status | Status | the workflow order |
| dept | Department | department code, then newest change first |
### Facets
`facets=1` on `GET /api/packets`, and always on the shop, stall, warehouse and Q.C. boards, adds the stations and assigned welders among the packets in the project and unit pick, ordered by name. The other filters do not narrow the facets. The find bar's Station and Worker lists come from them. The KPI numbers on every home screen follow the project and unit pick only, never the other filters.
### Exports
Every PM export writes one `print_log` row and one audit row with the action `report exported`, after the bytes are built (open question 39).
| Export | Log kind | what | pages |
|---|---|---|---|
| `GET /api/pm/export.csv` | `report-csv` | `<table name> · CSV · <scope label>` | none |
| `GET /api/pm/report.pdf` | `report-pdf` | `Production status report · PDF · <scope label>` | the page count |
The rows carry no packet and no unit. The audit detail carries `name`, `format`, `scope` and `period`. The CSV has the formula guard on every cell, a BOM and CRLF line ends. The PDF carries the controlled-copy stamp on every page.
### Presence
The packet page shows who else has the packet open: `Also in this packet now: <Name> (<role>)`, joined with ` · `. Nothing is shown when the person is alone.
- Any signed-in person sees who else is on the same packet. A presence entry is the user id, display name, role, screen, packet id and since. Nothing else (open question 40).
- The role is the label of the screen the person has open (`Clerical`, `Warehouse`, `Supervisor`, `Weld Stall`, `Q.C.`, `Project Mgmt`, `Admin`), else the label of their first role.
- A locked session drops out at once. Lock and unlock publish a `session` event (`locked`, `unlocked`) from `POST /api/auth/lock`, `POST /api/auth/unlock` and the server's idle lock. Unlock brings the person back.
- A revoked session drops out at once, and its stream ends.
- Closing the tab or losing the network drops the person out when the stream ends, within the stream's ping cycle (25 seconds). No other idle rule is needed.
- The server also keeps who is on each screen for the Admin view of phase 9. No endpoint reads it yet.
### Tightenings against the old build
| What the old build allowed | What the rebuild does | Why | Can be vetoed |
|---|---|---|---|
| The find bar filtered in the browser, over every packet the browser had loaded. | The server applies the search and every filter on each list and board. A filter only narrows what the person may read. | A board never fetches more than it shows. | No |
| "Mine" was worked out in the browser from the local session's role. | Mine is decided on the server from the person's AD roles and the screen they name. The stall's Mine needs the picked station. | The roles come from AD, not from what the browser keeps. | Yes |
| PM exports were built and saved in the browser, and the browser wrote the `report exported` log line. | Every export is built on the server and logged in `print_log` and the audit as `report exported`, with the scope, the period and, for the PDF, the page count. | The browser alone could skip the log. | Yes |
| The PM reports were on the PM home screen only. | Any signed-in role may read them, as any role may open any packet. PM still writes nothing. | The figures come from the same packets every role can already open. | Yes |
| Each browser published its own presence, with the name, employee ID, role and stall, refreshed every 20 seconds. | The server builds presence from the session: display name and role, no employee ID. A locked or revoked session drops out at once. | Presence shows only what it needs, and a locked screen is not someone in the packet. | Yes |
| Over target was worked out in the browser from the hours it had. | Over target is timer hours over the work-order target, on the server. Ticket hours are shown beside it and never decide it. | The station timer is the clock of record (open question 1). | Yes |
## Worker writes (phase 3)
The worker has no session. It writes through the same policy as a person, as the system principal `Principal(user_id=None, system="visual-feed")`. Each write locks the packet row (`FOR UPDATE SKIP LOCKED`; a row locked by someone else is left for the next run), builds the packet state, runs `policy.check()`, makes its change, bumps `version`, and writes a `packet_log` row (by nobody, by name "VISUAL feed", role `system`), an audit row (`packet.<action>`, role `system`) and a change event. See [FEED.md](FEED.md) for when each write happens.
+10 -3
View File
@@ -1,6 +1,6 @@
# Packet Station schema
Status: phases 1 to 4 built (identity, organisation, projects, packets, change orders, documents, jobs, VISUAL and drawing caches, the warehouse columns in use, the `notification_events` placeholder). Phase 5 adds the shop columns, the checklist templates and the clock rules (migration 0006). Phase 6 adds the Q.C. columns, the Q.C. check items, the Q.C. folder metadata, the ITP header, line and block details and the ITP log (migration 0007). Phase 7 adds the print log and the data book on the seal (migration 0008). Phase 9 adds the notification rules and delivery. The DDL is in `server/packetstation/migrations/`; this file explains it. See [PLAN.md](PLAN.md), "Schema".
Status: phases 1 to 4 built (identity, organisation, projects, packets, change orders, documents, jobs, VISUAL and drawing caches, the warehouse columns in use, the `notification_events` placeholder). Phase 5 adds the shop columns, the checklist templates and the clock rules (migration 0006). Phase 6 adds the Q.C. columns, the Q.C. check items, the Q.C. folder metadata, the ITP header, line and block details and the ITP log (migration 0007). Phase 7 adds the print log and the data book on the seal (migration 0008). Phase 8 adds no table and no migration: the PM report exports use new `print_log` kinds and presence uses two new session events. Phase 9 adds the notification rules and delivery. The DDL is in `server/packetstation/migrations/`; this file explains it. See [PLAN.md](PLAN.md), "Schema".
## Conventions
@@ -250,8 +250,8 @@ The texts may hold `{wps}` (else `the WPS`), `{stamp}` (`[75] ` with a trailing
| `at` | when, default `now()` |
| `by`, `by_name` | the user and a snapshot of the name |
| `session_id` | the session that asked |
| `packet_id`, `unit_id`, `itp_id` | what was printed; `unit_id` and `itp_id` for an ITP |
| `kind` | `traveler`, `map-h`, `map-w`, `clean`, `void`, `databook`, `doc`, `itp`, `csv`, `databook-export`, `itp-pdf` |
| `packet_id`, `unit_id`, `itp_id` | what was printed; `unit_id` and `itp_id` for an ITP; all three null for a PM report |
| `kind` | `traveler`, `map-h`, `map-w`, `clean`, `void`, `databook`, `doc`, `itp`, `csv`, `databook-export`, `itp-pdf`; phase 8 adds `report-csv` and `report-pdf` |
| `what` | the human label as printed: `Job traveler`, `Drawing with heat map (sheet 2)`, `MTR - item 3` |
| `pages` | the page count |
| `asset_id` | the document printed for kind `doc`; the filed data book for `databook` |
@@ -269,6 +269,13 @@ Indexed on (`packet_id`, `id`) and (`itp_id`, `id`). `GET /api/packets/{id}/prin
- The packet read shows the newest seal row, broken or not, with its data book, and `seals` (how many rows the packet has).
- The signed ITP PDF filed at sign-off is an `assets` row with `source` `itp_signoff`, stored on `itps.signoff_asset_id`.
## Project Management and presence (phase 8, no migration)
Phase 8 adds no table, no column and no migration. The Project Management reports read the existing tables (`packets`, `units`, `projects`, `packet_stages`, `packet_transitions`, `packet_wh`, `qc_history`, `bom_shorts`, `change_orders`, `visual_labor`) and never VISUAL.
- `print_log` gains two kinds, with no schema change (`kind` is free text): `report-csv` for a PM CSV export and `report-pdf` for the production status report. `packet_id`, `unit_id` and `itp_id` are null on these rows. `what` is `<table name> · CSV · <scope label>` or `Production status report · PDF · <scope label>`. `pages` is the page count for the PDF and null for the CSV. The audit row beside it has the action `report exported`.
- `ps_events` (the NOTIFY channel the SSE hub listens on) carries two new session events beside `revoked`: `{"event": "session", "session_id", "action": "locked"}` and `"unlocked"`. They are published by the lock and unlock endpoints and by the server's idle lock, where `sessions.locked_since` is set or cleared. The hub uses them to hide and show that session in presence. Presence itself lives in the app process, not in the database.
## Change orders (phases 2 and 3)
- `change_orders`: `co_no` (from `next_co_no`; unique per packet), `origin` (co_origin), `review` (co_review), `minor`, `hold`, `held_by`, `held_at`, `ecn`, `reason`, `issued_*`, `approved_*` and `rejected_*` (each with a `*_signature_id`), `vis`, `waiting` and `staged` jsonb (feed-shaped), `target_fp`, `row_version`. At most one pending VISUAL change order per packet (unique on `packet_id` where origin is visual and review is pending), so the feed re-aims it instead of stacking new ones.
+14 -2
View File
@@ -2,7 +2,7 @@
This document is rewritten for the server build. The old browser build's security document is in the old repo (`C:\Users\spencerm\Coding\PacketStation\docs\SECURITY.md`). The design comes from [BRIEF.md](BRIEF.md) and [PLAN.md](PLAN.md).
Status: phase 7. The identity controls (sessions, PIN, rate limits, audit, Origin check, derived group map) and the policy controls (server policy on every write, workflow steps as compare-and-set, server-side validation and limits, IT edit reasons, the filed lock, the upload pipeline for drawings and documents) are built and tested. Phase 3 adds the worker controls (read-only VISUAL and M-Files reads, stack guard, advisory locks, redacted errors, audited system writes). No requirement ID closes in phase 3. Phase 4 completes the upload pipeline (a kind per upload with its own types and limit, XLSX and CSV by content, the optional ClamAV scan, the assets volume quota alert) and adds the warehouse rules (the heat gate on release, closed hold and short reasons, receive by custody). Phase 4 closes D5. Phase 5 adds the shop rules (the server as the clock of record, server-assigned mark numbers under the packet row lock, insert-only change-order verifications with a signature, VOID sheets never edited). Phase 6 adds the Q.C. rules (server gates on accept, block sign and sign-off, NCR numbers checked by pattern, Q.C. documents never deleted, the append-only ITP log, server-verified signatures on every Q.C. and ITP sign-off). No requirement ID closes in phase 6. Phase 7 adds the close and file controls (the HMAC seal with the data book filed on it, the controlled-copy and print stamps signed with the seal key, `print_log` and audit rows for every print and export, the CSV formula guard, PDF metadata, escaping in the PDFs). Phase 7 closes M2, S6, S7 and S8. Every other control names the phase that builds it.
Status: phase 8. The identity controls (sessions, PIN, rate limits, audit, Origin check, derived group map) and the policy controls (server policy on every write, workflow steps as compare-and-set, server-side validation and limits, IT edit reasons, the filed lock, the upload pipeline for drawings and documents) are built and tested. Phase 3 adds the worker controls (read-only VISUAL and M-Files reads, stack guard, advisory locks, redacted errors, audited system writes). No requirement ID closes in phase 3. Phase 4 completes the upload pipeline (a kind per upload with its own types and limit, XLSX and CSV by content, the optional ClamAV scan, the assets volume quota alert) and adds the warehouse rules (the heat gate on release, closed hold and short reasons, receive by custody). Phase 4 closes D5. Phase 5 adds the shop rules (the server as the clock of record, server-assigned mark numbers under the packet row lock, insert-only change-order verifications with a signature, VOID sheets never edited). Phase 6 adds the Q.C. rules (server gates on accept, block sign and sign-off, NCR numbers checked by pattern, Q.C. documents never deleted, the append-only ITP log, server-verified signatures on every Q.C. and ITP sign-off). No requirement ID closes in phase 6. Phase 7 adds the close and file controls (the HMAC seal with the data book filed on it, the controlled-copy and print stamps signed with the seal key, `print_log` and audit rows for every print and export, the CSV formula guard, PDF metadata, escaping in the PDFs). Phase 7 closes M2, S6, S7 and S8. Phase 8 adds the Project Management reads and exports, the server-side find bar filters and the presence rules (display name and role only, locked and revoked sessions dropped). No requirement ID closes in phase 8. Every other control names the phase that builds it.
Contents:
@@ -107,7 +107,7 @@ The M: drive build had 20 layers of protection, most of them enforced in the bro
- A change-order verification needs a signature and is insert-only. A second one for the same department is refused. It cannot be withdrawn.
- VOID sheets are never edited. A voided page is kept, and new marks or moves on it are refused (422 `void_page`).
- A borrowed heat map is read-only. Nothing on the sibling packet can be written through the borrowing packet.
- Presence over SSE carries display names, never other personal data. Each entry is the user id, display name, screen and packet id.
- Presence over SSE carries display names, never other personal data. Each entry is the user id, display name, screen and packet id. Phase 8 adds the role and since (see "Project Management, find bar and presence").
### Q.C. rules (phase 6)
@@ -165,6 +165,18 @@ The M: drive build had 20 layers of protection, most of them enforced in the bro
- Responses carry `Content-Disposition` (inline, or attachment with `?download=1`), `X-Content-Type-Options: nosniff` and `Cache-Control: no-store`. A print of a document checks that the asset belongs to the packet, its unit or project folder, or its filed data book; anything else is 404.
- The signed ITP PDF is built and filed in the sign-off transaction. A PDF that cannot be built fails the sign-off.
### Project Management, find bar and presence (phase 8)
- Project Management reads only. Every endpoint under `/api/pm` is a GET and needs a signed-in session. Any role may read them, as any role may open any packet. The PM role passes no write action anywhere. The reports read PostgreSQL only, never VISUAL or M-Files.
- Every PM export is built on the server and logged: one `print_log` row (kind `report-csv` or `report-pdf`, no packet, the scope label in `what`, the page count for the PDF) and one audit row with the action `report exported` and the detail `{name, format, scope, period}`. `print_log` stays append-only.
- The PM CSV has the formula guard on every cell (S6), a BOM and CRLF line ends. The PDF report carries the controlled-copy stamp on every page and the PDF metadata. Both answer with `Content-Disposition`, `nosniff` and `no-store`, as the phase 7 exports do.
- Filters never widen access. The search, the status, flag, station, worker and Mine filters are applied by the server inside each list's and board's own scope (a supervisor's departments, the stall's station). A filter can only narrow what the person may already read.
- Search words are bound parameters, matched with ILIKE and the LIKE escape. A query keeps at most 8 words of at most 60 characters each. IDs are matched as text, never cast.
- Mine is decided on the server from the person's AD roles and the screen they name. Nothing the browser keeps can widen it.
- Presence shows the display name and the role only (the user id, screen, packet id and since travel with it). No employee ID, no email, no other personal data. The server builds it from the session, not from what the browser says about itself.
- A locked session drops out of presence at once, and comes back on unlock. A revoked session drops out at once and its stream ends. A closed tab drops out when the stream ends, within the 25-second ping cycle.
- Saved filters live in the browser's localStorage (`ps.flt`, `ps.proj`, `ps.pmtab`, `ps.pmper`, `ps.motion`). They hold filter choices only. The search text is never saved. Nothing in them is trusted by the server.
### Headers (phase 0)
- The app sets HSTS, CSP and the other security headers itself. NPM's own HSTS is off.