From 6365586e8267a8ced4c6c86555a150dda7a4f9f0 Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:34:56 +0200 Subject: [PATCH] M28: complete production acceptance --- PROJECT_STATE.md | 36 ++++++++++++++++++++++++++++++++++++ frontend/e2e/privacy.spec.ts | 3 ++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 6070657..cf71768 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -2611,3 +2611,39 @@ evidence yet." vulnerabilities. - Exact next action: commit/push, take a verified pre-deployment backup, redeploy to Unraid, then run migration, health, browser and full Playwright acceptance. + +## M27 — portable backup evidence (2026-08-10) + +- Live restore-readiness validation found that scheduled checksum files named their dump + with the container-only `/backups` path. Dumps were structurally valid, but host-side + `sha256sum -c` could not resolve that path. +- Scheduled and on-demand backups now write basename-only checksum entries, so dump plus + sidecar remains verifiable after copying to the host or an independent secondary + destination. +- Evidence: Alpine shell syntax passed; the recreated live backup service produced + `mobilityops-20260810T141944Z.dump`; checksum validation and `pg_restore --list` both + passed through the documented host verification command. + +## M28 — final production acceptance and hand-off (2026-08-10) + +- Pushed milestones M21–M27 to Gitea `master`, deployed committed source archives to + `/mnt/user/appdata/mobilityops`, and applied migration `b913a72e8c14 (head)`. Public + passwordless demo access remains enabled; OIDC remains an optional disabled addition. +- Activated the pinned internal Prometheus/Grafana profile with a server-generated + Grafana password and loopback-only ports. Prometheus reports the API target up; all six + alert rules evaluate healthy; Grafana 12.2.0 reports database `ok`. API, database, web, + scheduled backup, Prometheus and Grafana are running, while the existing shared n8n + remains the only n8n instance used by Fleet Ops. +- Live visual acceptance covered desktop dashboard and knowledge statistics plus Privacy + at desktop and a fresh 390 × 844 load. There was no horizontal overflow; the mobile + shell starts closed and the irreversible privacy action remains guarded. +- Complete live Playwright acceptance is **147 passed in 4.6 minutes**. A race in the new + privacy test login helper was removed by waiting for the authenticated dashboard; the + privacy pair also passed three consecutive repetitions before the full green run. +- Final demo reset restored **2 users / 180 customers / 50 vehicles / 254 bookings / 75 + inspections / 40 maintenance records / 33 quality issues / 20 workflow runs**. API + readiness is `ready`, RAGcore is available with 11 authoritative NL sources and an + honestly unknown provider index count, and the latest backup verifies. No API/backup + error or traceback appears in post-deployment logs. +- Exact next action: none for the selected scope. Optional secret rotation and new n8n + execution evidence remain intentionally deferred by the user. diff --git a/frontend/e2e/privacy.spec.ts b/frontend/e2e/privacy.spec.ts index d2e1b47..4ffce71 100644 --- a/frontend/e2e/privacy.spec.ts +++ b/frontend/e2e/privacy.spec.ts @@ -5,13 +5,14 @@ async function resetAndLogin(request: APIRequestContext, page: Page) { await request.post("/api/v1/demo/reset"); await page.goto("/login"); await page.getByRole("button", { name: "Verken als Operationsmanager" }).click(); + await expect(page).toHaveURL(/\/dashboard$/); } test("privacy centre reports policy and produces an audited CSV export", async ({ page, request }) => { await resetAndLogin(request, page); await page.goto("/privacy"); await expect(page.getByRole("heading", { name: "Privacybeheer" })).toBeVisible(); - await expect(page.getByText("30 dagen")).toBeVisible(); + await expect(page.getByText("30 dagen", { exact: true })).toBeVisible(); const downloadPromise = page.waitForEvent("download"); await page.getByRole("link", { name: "Audit CSV downloaden" }).click(); const download = await downloadPromise;