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
+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.