Phase 10: regression bench, hardening, archive location, native VM kit, go-live docs #55
+2
-1
@@ -25,6 +25,7 @@ All notable changes to this project are recorded here. The format follows [Keep
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- The hand drawing, hand BOM and rev approval lines on a VISUAL draft showed the person's user id instead of their name.
|
||||||
- The MTR viewer showed PDF certificates in a frame the server's own headers refuse, so Chrome showed nothing. It now draws them with the drawing viewer.
|
- The MTR viewer showed PDF certificates in a frame the server's own headers refuse, so Chrome showed nothing. It now draws them with the drawing viewer.
|
||||||
|
|
||||||
- Gitea issue 42: work-order text on the traveler stays inside its own box. Each line shrinks down to 70 percent of its size, then is cut with "...", so a long work order, operation or drawing no longer runs over the next QR code. The gap between work-order boxes is 18 pt, a quiet zone for the kiosk scanner. On the Work orders tab the card text wraps inside the card, the work order is 22 px and cards are at least 340 px wide, one per row on a narrow screen.
|
- Gitea issue 42: work-order text on the traveler stays inside its own box. Each line shrinks down to 70 percent of its size, then is cut with "...", so a long work order, operation or drawing no longer runs over the next QR code. The gap between work-order boxes is 18 pt, a quiet zone for the kiosk scanner. On the Work orders tab the card text wraps inside the card, the work order is 22 px and cards are at least 340 px wide, one per row on a narrow screen.
|
||||||
@@ -62,7 +63,7 @@ All notable changes to this project are recorded here. The format follows [Keep
|
|||||||
- Phase 10: archive location (Gitea issue 7). With `ARCHIVE_ROOT` set, an Admin picks the archive folder under Admin, Storage, File locations. The folder must be under the root: `..`, absolute paths elsewhere and links that leave the root are refused, and nothing creates folders. Check writes, reads back and removes a probe file. Save runs the same check first. Each filing is copied to `<folder>/<project>/<unit>/<data book>-seal<id>/`: the sealed data book, the drawing pages, the packet documents, the unit's and project's Q.C. folder documents, the MTRs and a `manifest.json` with every file's sha256. The new `archive-copy` job runs every 15 minutes and right after each filing. A failed copy never blocks filing. It is retried after 5, 15 and 60 minutes, then hourly, and raises the critical `archive_failed` notification after 3 tries; "Retry now" asks for a run. Turning the archive on later also copies earlier filings. Nothing in the archive is overwritten or deleted. Every setting change, check and copy is audited. `packetstation doctor` checks the folder. Migration 0012 adds `archive_copies`.
|
- Phase 10: archive location (Gitea issue 7). With `ARCHIVE_ROOT` set, an Admin picks the archive folder under Admin, Storage, File locations. The folder must be under the root: `..`, absolute paths elsewhere and links that leave the root are refused, and nothing creates folders. Check writes, reads back and removes a probe file. Save runs the same check first. Each filing is copied to `<folder>/<project>/<unit>/<data book>-seal<id>/`: the sealed data book, the drawing pages, the packet documents, the unit's and project's Q.C. folder documents, the MTRs and a `manifest.json` with every file's sha256. The new `archive-copy` job runs every 15 minutes and right after each filing. A failed copy never blocks filing. It is retried after 5, 15 and 60 minutes, then hourly, and raises the critical `archive_failed` notification after 3 tries; "Retry now" asks for a run. Turning the archive on later also copies earlier filings. Nothing in the archive is overwritten or deleted. Every setting change, check and copy is audited. `packetstation doctor` checks the folder. Migration 0012 adds `archive_copies`.
|
||||||
- Phase 10: a banner tells everyone when the server was updated ("Packet Station was updated. Reload to get the new version.") with a Reload button. It checks on focus, on return to the tab, on an event-stream reconnect and every 5 minutes. It never reloads by itself.
|
- Phase 10: a banner tells everyone when the server was updated ("Packet Station was updated. Reload to get the new version.") with a Reload button. It checks on focus, on return to the tab, on an event-stream reconnect and every 5 minutes. It never reloads by itself.
|
||||||
- Phase 10: native VM install kit in `deploy/vm/` for production and test, each on its own Linux VM without Docker: systemd units for the app, the worker, the migration and a firewall that lets only NPM reach port 8000; a launcher that hands each unit only its own secrets; `install.sh` (PostgreSQL 16, ODBC Driver 18, the PESCO CA, the service user and folders) and `upgrade.sh` with rollback; `ps-run`, `ps-doctor`, env examples with explicit `VISUAL_SERVER` and `VISUAL_DATABASE`. DEPLOY.md has the steps, the NPM proxy hosts for the VMs and the archive NFS mount.
|
- Phase 10: native VM install kit in `deploy/vm/` for production and test, each on its own Linux VM without Docker: systemd units for the app, the worker, the migration and a firewall that lets only NPM reach port 8000; a launcher that hands each unit only its own secrets; `install.sh` (PostgreSQL 16, ODBC Driver 18, the PESCO CA, the service user and folders) and `upgrade.sh` with rollback; `ps-run`, `ps-doctor`, env examples with explicit `VISUAL_SERVER` and `VISUAL_DATABASE`. DEPLOY.md has the steps, the NPM proxy hosts for the VMs and the archive NFS mount.
|
||||||
- Phase 10: regression bench. The old build's 38 scenarios are mapped to Playwright specs in `tests/e2e/BENCH.md`; the missing ones are new specs on synthetic fixtures, and every scenario fails on a CSP violation.
|
- Phase 10: regression bench. The old build's 38 scenarios are mapped to Playwright specs in `tests/e2e/BENCH.md`; the missing ones are new specs on synthetic fixtures (111 Playwright tests, 4.5 minutes), and every test fails on a CSP violation.
|
||||||
- Phase 10: GO-LIVE.md (date and feed boundary, groups, tablets, URL, cut-over, retiring M: as a read-only archive, production start, checklist, incident notes) and PAPER-FALLBACK.md (what each department does while the server is down and how paper is entered back).
|
- Phase 10: GO-LIVE.md (date and feed boundary, groups, tablets, URL, cut-over, retiring M: as a read-only archive, production start, checklist, incident notes) and PAPER-FALLBACK.md (what each department does while the server is down and how paper is entered back).
|
||||||
|
|
||||||
- Gitea issue 18 (change C7): the department supervisor approves every received short, one receipt at a time, with a signature ("Approve a received short"), after checking the part and its heat number against what the warehouse issued. A lead is not enough. The Materials tab lists each receipt with who received it and who approved it, and an Approve button for the supervisor. A packet cannot be accepted as complete, or filed, until every short is received and approved ("Every short must be received and approved by the supervisor first."). Accept and route to another department does not wait. `POST /api/packets/{id}/shorts/receipts/{receipt_id}/approve`; the packet read gains `shorts` and `shorts_unapproved`. Migration 0011 adds the append-only `short_approvals` table.
|
- Gitea issue 18 (change C7): the department supervisor approves every received short, one receipt at a time, with a signature ("Approve a received short"), after checking the part and its heat number against what the warehouse issued. A lead is not enough. The Materials tab lists each receipt with who received it and who approved it, and an Approve button for the supervisor. A packet cannot be accepted as complete, or filed, until every short is received and approved ("Every short must be received and approved by the supervisor first."). Accept and route to another department does not wait. `POST /api/packets/{id}/shorts/receipts/{receipt_id}/approve`; the packet read gains `shorts` and `shorts_unapproved`. Migration 0011 adds the append-only `short_approvals` table.
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ Each phase ends with passing tests, docs updated (README, CHANGELOG, SECURITY.md
|
|||||||
- Production and test each on their own Linux VM, natively, with no Docker (Spencer, 2026-10-27, Gitea issue 37; see "Host layout"). The test VM is built first from the same steps, so the production install, backups, restore and doctor are rehearsed before go-live. The app and the worker run as systemd services from a venv built from the same `server/requirements.txt` and `server/requirements-pesco.txt` pins as the image, with PostgreSQL 16, ODBC Driver 18 and the PESCO CA installed on the VM. Settings from an env file, secrets as root-owned files read through the same `*_PASSWORD_FILE` settings, VISUAL_SERVER and VISUAL_DATABASE set explicitly; first start is Spencer's approval. NPM on docker-1 proxies `packetstation.pescoinc.biz` and `packetstation-test.pescoinc.biz` to their VMs over the LAN. Backups, the doctor and the restore are rewritten for the VMs (no `docker exec`). DEPLOY.md gets a VM section; the compose files stay for CI. The docker-1 test stack keeps running until the test VM is ready, then it is stopped.
|
- Production and test each on their own Linux VM, natively, with no Docker (Spencer, 2026-10-27, Gitea issue 37; see "Host layout"). The test VM is built first from the same steps, so the production install, backups, restore and doctor are rehearsed before go-live. The app and the worker run as systemd services from a venv built from the same `server/requirements.txt` and `server/requirements-pesco.txt` pins as the image, with PostgreSQL 16, ODBC Driver 18 and the PESCO CA installed on the VM. Settings from an env file, secrets as root-owned files read through the same `*_PASSWORD_FILE` settings, VISUAL_SERVER and VISUAL_DATABASE set explicitly; first start is Spencer's approval. NPM on docker-1 proxies `packetstation.pescoinc.biz` and `packetstation-test.pescoinc.biz` to their VMs over the LAN. Backups, the doctor and the restore are rewritten for the VMs (no `docker exec`). DEPLOY.md gets a VM section; the compose files stay for CI. The docker-1 test stack keeps running until the test VM is ready, then it is stopped.
|
||||||
- Archive location (Gitea issue 7, Spencer 2026-10-27). Spencer creates an NFS share on the QNAP and mounts it on docker-1 (root, steps in DEPLOY.md), with write access for uid 10001. Compose bind-mounts it into the app and the worker at a fixed root, `ARCHIVE_ROOT` (unset turns the feature off). The Admin page gets a File locations section with one setting for now, the archive folder, as a path under `ARCHIVE_ROOT`. The server refuses a path that leaves the root (`..`, absolute paths elsewhere, symlinks out). "Check" writes a probe file, reads it back, removes it and shows the result, and saving runs the same check first. The setting change and every check are audited. `doctor` runs the check too. At filing, the sealed data book, each of the packet's documents and a manifest are written to the archive folder (SethD, 2026-10-28, issue 7). Whether a failed copy blocks the filing or is retried and raised to Admin is settled in the phase 10 design.
|
- Archive location (Gitea issue 7, Spencer 2026-10-27). Spencer creates an NFS share on the QNAP and mounts it on docker-1 (root, steps in DEPLOY.md), with write access for uid 10001. Compose bind-mounts it into the app and the worker at a fixed root, `ARCHIVE_ROOT` (unset turns the feature off). The Admin page gets a File locations section with one setting for now, the archive folder, as a path under `ARCHIVE_ROOT`. The server refuses a path that leaves the root (`..`, absolute paths elsewhere, symlinks out). "Check" writes a probe file, reads it back, removes it and shows the result, and saving runs the same check first. The setting change and every check are audited. `doctor` runs the check too. At filing, the sealed data book, each of the packet's documents and a manifest are written to the archive folder (SethD, 2026-10-28, issue 7). Whether a failed copy blocks the filing or is retried and raised to Admin is settled in the phase 10 design.
|
||||||
- Closes: O3, O4; S4 is IT's pentest, deferred.
|
- Closes: O3, O4; S4 is IT's pentest, deferred.
|
||||||
|
- Done 2026-10-30. The bench maps all 38 old scenarios (tests/e2e/BENCH.md; trel is open question 54) and runs 111 Playwright tests, each failing on a CSP violation. The CSP dropped `'unsafe-inline'`. pip-audit found pypdf, pyasn1 and certifi to bump. The archive copy, the version banner, the VM kit in `deploy/vm/` with VM backups and restore, and the go-live, paper-fallback and security docs are built. The VMs, the NFS share, the timers and the go-live date are Spencer's and Seth's (open question 53 records the archive copy policy).
|
||||||
|
|
||||||
## SECURITY.md Part 3: which phase closes each ID
|
## SECURITY.md Part 3: which phase closes each ID
|
||||||
|
|
||||||
@@ -292,6 +293,7 @@ From phase 9 (2026-10-21), for Seth or Spencer to veto:
|
|||||||
51. A direct welder assign: default it does not check `profile_active`, so an Admin's inactive switch hides a person from the picker but a typed assign still works. Phase 9 (2026-10-21) applied the default; say if an inactive profile should be refused there too.
|
51. A direct welder assign: default it does not check `profile_active`, so an Admin's inactive switch hides a person from the picker but a typed assign still works. Phase 9 (2026-10-21) applied the default; say if an inactive profile should be refused there too.
|
||||||
52. A checklist item added while a stage is open: default it applies to that open stage too, because completeness reads the live template, so the item must be ticked before submit. Phase 9 (2026-10-21) applied the default. Replaced by change C8: a template change applies to new packets only. Built on 2026-10-30: a stage freezes its list at START, so a change reaches only stages not yet started.
|
52. A checklist item added while a stage is open: default it applies to that open stage too, because completeness reads the live template, so the item must be ticked before submit. Phase 9 (2026-10-21) applied the default. Replaced by change C8: a template change applies to new packets only. Built on 2026-10-30: a stage freezes its list at START, so a change reaches only stages not yet started.
|
||||||
53. A failed archive copy at filing (phase 10, Gitea issue 7): default it never blocks the filing. The FILE transaction asks the worker for an `archive-copy` run; the job retries a failed copy after 5, 15 and 60 minutes, then hourly, and raises the `archive_failed` condition to Admin after 3 failed tries or a failed folder check. Admin, Storage, File locations shows the failures and has "Retry now". Turning the archive on later also copies the earlier filings. Seth or Spencer may ask for the stricter rule (the copy must succeed before the packet counts as filed) instead.
|
53. A failed archive copy at filing (phase 10, Gitea issue 7): default it never blocks the filing. The FILE transaction asks the worker for an `archive-copy` run; the job retries a failed copy after 5, 15 and 60 minutes, then hourly, and raises the `archive_failed` condition to Admin after 3 failed tries or a failed folder check. Admin, Storage, File locations shows the failures and has "Retry now". Turning the archive on later also copies the earlier filings. Seth or Spencer may ask for the stricter rule (the copy must succeed before the packet counts as filed) instead.
|
||||||
|
54. The old Clerical "Released" section (every released packet by place, with overdue and 7+ day counts; old bench scenario trel) is not in the new build and not on the Drop list (phase 10, tests/e2e/BENCH.md). Default: not built; the boards and the find bar's Mine, Board and List views show where each packet is. Seth decides whether it is needed for parity.
|
||||||
|
|
||||||
Changes decided by SethD on 2026-10-26. Each replaces a default above. All five were built on 2026-10-30 (branch `seth-changes`):
|
Changes decided by SethD on 2026-10-26. Each replaces a default above. All five were built on 2026-10-30 (branch `seth-changes`):
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ The brief's wording for the identity IDs, which the table follows: I4 and I7 are
|
|||||||
|
|
||||||
| ID | Requirement | Status | Owner |
|
| ID | Requirement | Status | Owner |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| S1 | CSP, X-Frame-Options and related headers | Closed, phase 0, tuned phase 10 (the CSP pass; see "Headers"). | Dev |
|
| S1 | CSP, X-Frame-Options and related headers | Closed, phase 0, tuned phase 10 (the CSP pass; see "Headers"; every e2e test fails on a CSP violation and one checks the served policy exactly). | Dev |
|
||||||
| S2 | CSRF protection on every state-changing request | Closed, phase 1 (SameSite=Strict cookie, the Origin check, the `X-Requested-With` header). | Dev |
|
| S2 | CSRF protection on every state-changing request | Closed, phase 1 (SameSite=Strict cookie, the Origin check, the `X-Requested-With` header). | Dev |
|
||||||
| S3 | Dependency update and vulnerability scan | Closed, phase 9 (CI scanning on every run: `pip-audit` over every pin in `server/requirements.txt` fails on any known vulnerability; `npm audit --audit-level=high` fails on a high or critical one). Phase 10 bumped pypdf to 6.21.4, certifi to 2026.10.23 and pyasn1 to 0.6.5, and the scan is clean. | Dev |
|
| S3 | Dependency update and vulnerability scan | Closed, phase 9 (CI scanning on every run: `pip-audit` over every pin in `server/requirements.txt` fails on any known vulnerability; `npm audit --audit-level=high` fails on a high or critical one). Phase 10 bumped pypdf to 6.21.4, certifi to 2026.10.23 and pyasn1 to 0.6.5, and the scan is clean. | Dev |
|
||||||
| S4 | Security review or penetration test before go-live | Deferred to IT: a penetration test of the production VM and the proxy before go-live, by IT or an outside firm. Who does it and when is TBD (Spencer decides). It is on the go-live checklist. If go-live goes ahead without it, Spencer records that decision on the checklist. | IT |
|
| S4 | Security review or penetration test before go-live | Deferred to IT: a penetration test of the production VM and the proxy before go-live, by IT or an outside firm. Who does it and when is TBD (Spencer decides). It is on the go-live checklist. If go-live goes ahead without it, Spencer records that decision on the checklist. | IT |
|
||||||
|
|||||||
@@ -927,6 +927,7 @@ export interface HandUpload {
|
|||||||
name?: string | null;
|
name?: string | null;
|
||||||
reason?: string | null;
|
reason?: string | null;
|
||||||
by?: WhoRef | string | null;
|
by?: WhoRef | string | null;
|
||||||
|
by_name?: string | null;
|
||||||
at?: string | null;
|
at?: string | null;
|
||||||
vis_rev?: string | null;
|
vis_rev?: string | null;
|
||||||
lines?: number;
|
lines?: number;
|
||||||
@@ -974,6 +975,7 @@ export interface VisualInfo {
|
|||||||
rev: string | number;
|
rev: string | number;
|
||||||
vis_rev?: string | null;
|
vis_rev?: string | null;
|
||||||
by?: WhoRef | string | null;
|
by?: WhoRef | string | null;
|
||||||
|
by_name?: string | null;
|
||||||
at?: string | null;
|
at?: string | null;
|
||||||
reason?: string | null;
|
reason?: string | null;
|
||||||
/** "called": Clerical kept the rev VISUAL calls over M-Files' Effective rev (`latest`). */
|
/** "called": Clerical kept the rev VISUAL calls over M-Files' Effective rev (`latest`). */
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ describe("Packet page, VISUAL box", () => {
|
|||||||
it("uploads a hand BOM as multipart with the signature fields", async () => {
|
it("uploads a hand BOM as multipart with the signature fields", async () => {
|
||||||
const fetch = mockFetch({
|
const fetch = mockFetch({
|
||||||
...base(visualDraft()),
|
...base(visualDraft()),
|
||||||
"POST /api/packets/1/hand/bom": { body: visualDraft({ version: 4, visual: { manual_bom: { name: "bom.csv", reason: "No materials yet", lines: 3 } } }) },
|
"POST /api/packets/1/hand/bom": { body: visualDraft({ version: 4, visual: { manual_bom: { name: "bom.csv", reason: "No materials yet", lines: 3, by: "cf284ce9-e7a6-45d0-bba6-d46b752d553d", by_name: "Clara Clerk" } } }) },
|
||||||
});
|
});
|
||||||
renderApp("/packets/1");
|
renderApp("/packets/1");
|
||||||
fireEvent.click(await screen.findByRole("button", { name: "Upload BOM" }));
|
fireEvent.click(await screen.findByRole("button", { name: "Upload BOM" }));
|
||||||
@@ -165,6 +165,9 @@ describe("Packet page, VISUAL box", () => {
|
|||||||
expect((fd.get("file") as File).name).toBe("bom.csv");
|
expect((fd.get("file") as File).name).toBe("bom.csv");
|
||||||
expect(await screen.findByRole("button", { name: "Use VISUAL's BOM again" })).toBeInTheDocument();
|
expect(await screen.findByRole("button", { name: "Use VISUAL's BOM again" })).toBeInTheDocument();
|
||||||
expect(screen.getByText("BOM uploaded by hand:", { exact: false })).toBeInTheDocument();
|
expect(screen.getByText("BOM uploaded by hand:", { exact: false })).toBeInTheDocument();
|
||||||
|
// The line names the person, never the stored user id.
|
||||||
|
expect(screen.getByText(/BOM uploaded by hand:.*Clara Clerk/)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/cf284ce9/)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uploads a hand drawing: the asset first, then the attach with reason and sign", async () => {
|
it("uploads a hand drawing: the asset first, then the attach with reason and sign", async () => {
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ type AskReason = (opts: ReasonOptions) => Promise<string | null>;
|
|||||||
const isCl = (roles: readonly string[]) => roles.some((r) => r === "clerical" || r === "clerical_supervisor" || r === "admin");
|
const isCl = (roles: readonly string[]) => roles.some((r) => r === "clerical" || r === "clerical_supervisor" || r === "admin");
|
||||||
const isClSup = (roles: readonly string[]) => roles.some((r) => r === "clerical_supervisor" || r === "admin");
|
const isClSup = (roles: readonly string[]) => roles.some((r) => r === "clerical_supervisor" || r === "admin");
|
||||||
|
|
||||||
function whoName(by: WhoRef | string | null | undefined): string {
|
/** The server stores `by` as the user id and the display name beside it in `by_name`. */
|
||||||
if (!by) return "";
|
function whoName(m: { by?: WhoRef | string | null; by_name?: string | null }): string {
|
||||||
return typeof by === "string" ? by : by.display_name;
|
if (m.by_name) return m.by_name;
|
||||||
|
if (!m.by) return "";
|
||||||
|
return typeof m.by === "string" ? m.by : m.by.display_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
function foldedText(f: FoldedOp): string {
|
function foldedText(f: FoldedOp): string {
|
||||||
@@ -126,7 +128,7 @@ function HandLine({ what, m }: { what: string; m: HandUpload }) {
|
|||||||
return (
|
return (
|
||||||
<div className="small">
|
<div className="small">
|
||||||
{what} uploaded by hand: <span className="mono">{m.name ?? ""}</span>
|
{what} uploaded by hand: <span className="mono">{m.name ?? ""}</span>
|
||||||
{m.by ? ` - ${whoName(m.by)}` : ""} {m.at ? fmtTime(m.at) : ""}
|
{m.by ? ` - ${whoName(m)}` : ""} {m.at ? fmtTime(m.at) : ""}
|
||||||
{m.reason ? ` · ${m.reason}` : ""}
|
{m.reason ? ` · ${m.reason}` : ""}
|
||||||
{typeof m.lines === "number" ? ` · ${m.lines} line${m.lines === 1 ? "" : "s"}` : ""}
|
{typeof m.lines === "number" ? ` · ${m.lines} line${m.lines === 1 ? "" : "s"}` : ""}
|
||||||
</div>
|
</div>
|
||||||
@@ -237,7 +239,7 @@ export function VisualBox({
|
|||||||
<b>Rev {String(v.rev_ok.rev)} approved by hand</b> in place of VISUAL rev {v.rev_ok.vis_rev || "-"}
|
<b>Rev {String(v.rev_ok.rev)} approved by hand</b> in place of VISUAL rev {v.rev_ok.vis_rev || "-"}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{v.rev_ok.by ? ` - ${whoName(v.rev_ok.by)}` : ""} {v.rev_ok.at ? fmtTime(v.rev_ok.at) : ""}
|
{v.rev_ok.by ? ` - ${whoName(v.rev_ok)}` : ""} {v.rev_ok.at ? fmtTime(v.rev_ok.at) : ""}
|
||||||
{v.rev_ok.reason ? `: ${v.rev_ok.reason}` : ""}.
|
{v.rev_ok.reason ? `: ${v.rev_ok.reason}` : ""}.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Reference in New Issue
Block a user