feat(admin): migrate Users with safe recovery and action state

This commit is contained in:
cesnimda
2026-10-20 15:10:04 +02:00
parent 8af9168014
commit 7ecc0d6605
10 changed files with 669 additions and 69 deletions
+13
View File
@@ -0,0 +1,13 @@
# P7-10 — Administration
Entry: P7-09 **8af9168014631633e38e8ba7cd22899fdf1092e2**, normally pushed and directly verified **20 October 2026 07:58:49 UTC**, clean development HEAD=origin=remote. It completes the implemented Settings scope with 47/6 affected frontend, 22 existing backend contracts and 23 final native cases; manual/provider/output/production qualifications remain.
Follow the [approved handoff](../../phase-6/p6-05/PHASE7_HANDOFF.md): exactly three separately validated and pushed boundaries, **Users → Audit → System**. Reuse existing routes, MUI/DataGrid, read/mutation owners and shared page/state components. No aggregate Admin dashboard or new API/schema contract.
| Boundary | State |
|---|---|
| [Users](USERS.md) | Implemented CONDITIONAL GO; final hygiene/publication receipt follows |
| Audit | Pending; preserve the exact supported Undo allowlist and ordinary owner-filtered Restore distinction |
| System | Pending; preserve independent SMTP/probe/test/maintenance scopes, write-only secret semantics and truthful diagnostic availability |
All 57 preservation groups retain **56 PARTIAL / C11 DEFERRED**, with 33 routes. P5-13 is withheld; followMode remains deferred. No Phase 8 implementation. Production readiness remains NO-GO; the independently owner-authorized main release is still unverified after run831 failed with browser crashes/malformed Playwright JavaScript. Do not infer admin recent-auth enforcement: the current Users/Audit/System actions do not implement that separate guard. Preserve actual account-lifecycle/export recent-auth checks and record this gap rather than inventing a security protocol during presentation migration.
+25
View File
@@ -0,0 +1,25 @@
# P7-10 Users
**Implemented CONDITIONAL GO; final hygiene and publication pending.** This bounded change preserves `/admin/users` and the existing authenticated administrator API contract. The shell retains the route h1; the owner uses the shared h2 header, h3 create section and existing desktop DataGrid/mobile cards.
## Findings and changes
The previous list catch converted every error to an empty list. Creation serialized a ProblemDetails object as `[object Object]`, and pending actions had no synchronous admission or disposal guard. A successful create could clear newer form values or start another private list read after unmount. Five new ordinary regressions failed against the prior owner while all four original cases passed.
The page now uses existing `useViewResource`/`ViewStateNotice` for distinct loading, unavailable/denied and actual empty states. Retry only reloads the list. A native create form retains the exact email/password/Admin payload. All mutations share synchronous admission and current-session/lifetime checks, current-language feedback and a named pending focus target. Creation clears only submitted fields that have not changed; a committed create followed by failed refresh offers read recovery without replay. Accepted deletion HTTP202 is labelled queued, not completed. Server last-admin/self-demotion/exact-email/delete availability remain authoritative.
Shared section/surface/density tokens replace owner-specific decorative surfaces. Mobile identities wrap. Desktop two-dimensional grid scrolling is intentional, named and keyboard reachable, with root overflow still checked. Final visual review exposed clipped wrapped action buttons; a strengthened browser assertion failed ordinarily before adding content-sized DataGrid rows. It now verifies every row action fits vertically and a horizontally scrolled action receives visible keyboard focus. Sorting, pagination and row identity remain unchanged.
## Evidence
- Frontend **18/18 cases in five suites PASS**: ten Users cases, including six additions, plus eight shared confirmation/dialog cases. Original self-demotion warning, other-admin warning, last-admin disable and exact-email confirmation remain ordinary passing cases. Queued-deletion feedback is tested with a mocked202, not an actual deletion.
- Existing backend **25/25 PASS**, zero skipped, covering Users controllers, API administrator authorization and session-context middleware with the unchanged Release binary.
- Final native **3/3 PASS**, zero skips/flaky/retries, **13:07:56–13:08:20 UTC**, export **`f6H1YeLZWstdE-yoN5W8B`**. Real isolated HTTP verifies administrator creation/role readback, revoked target sessions, non-admin403, last-admin409, deletion-disabled503 with retained account, GET-only503 recovery and keyboard creation. No reset email or actual account deletion runs.
- English/Bokmål light/dark samples at320,390,768,1024,1440,1920; seven all-enabled axe scans, zero violations,28 incomplete nodes. These are finite paired samples, not the full Cartesian product.
- App/browser TypeScript, lint and production build PASS. The intermediate compiler-memoization lint failure and its correction remain retained. [Portable results](evidence/users-results.json) record command/result hashes and explicitly post-run source identities.
Both final320px Bokmål dark and1440px English light PNGs were actually inspected after the row correction. The other five images are retained automated evidence. Earlier candidate images and clipped-row RED remain historical evidence. Incompletes include invisible badges/overlapping backgrounds, the inherited roleless shell version badge and MUI DataGrid sort-tooltip wrapper. These are retained accessibility qualifications for the shared-owner integration; no axe rule or application assertion is disabled. Human screen-reader, physical touch/software keyboard, actual zoom and production output checks remain pending.
## Preservation and remaining work
PRES-A01/E09 and affected shared interaction contracts gain bounded proof without changing the 57-group disposition or33routes. No backend authorization, tenant data, lifecycle flag, CV renderer/PDF/revision or AI safeguard changes. Current admin actions lack a separate recent-auth guard; this change does not claim otherwise. Actual SMTP/reset delivery, accepted deletion completion, provider/model/system operations and production remain unverified. Audit and System follow as separate commits. Phase 8 remains outside scope.
@@ -0,0 +1,259 @@
{
"observedUtc": "2026-10-20T13:08:49.381408+00:00",
"sourceScope": "Post-run observations, not pre-run captures",
"sources": {
"src/views/AdminUsersPage.tsx": "fc02891601b6dd5593210f872db86d18447e848955d60019350e1cda622761f0",
"src/admin-users-page.test.tsx": "ad167fc0f457ac082335edd23f729bce35d2246217337891f1f07fb9ccae9f71",
"src/i18n/translations.ts": "69ef9e733534731206a98b717e586a61ce1b78f6ed7b1b847b46b60b71ce0c32",
"e2e/phase7-admin-users.spec.ts": "53e2f7daa55627b66c20fc969ce553a6a73ea027da644be2531bcd467063c8f8"
},
"gates": {
"typecheck": {
"path": ".artifacts/phase7/current/ui/typecheck-command.json",
"sha256": "92b1a8a58e4f2a5b49dbcc17bb1f7a164b437b75a77ce29dbd06a6ed5d00ffaf",
"name": "typecheck",
"command": [
"rtk",
"proxy",
"npm.cmd",
"run",
"typecheck"
],
"startedUtc": "2026-10-20T13:07:13.238238+00:00",
"finishedUtc": "2026-10-20T13:07:23.292112+00:00",
"exitCode": 0
},
"lint": {
"path": ".artifacts/phase7/current/ui/lint-command.json",
"sha256": "e380e1fe5db37416bfae32f67fed85bb4827973220144fd7abdff63d842f012d",
"name": "lint",
"command": [
"rtk",
"proxy",
"npm.cmd",
"run",
"lint"
],
"startedUtc": "2026-10-20T13:07:28.787691+00:00",
"finishedUtc": "2026-10-20T13:07:48.996027+00:00",
"exitCode": 0
},
"browser-typecheck": {
"path": ".artifacts/phase7/current/ui/browser-typecheck-command.json",
"sha256": "1916ff75a302812d544bb7d0095efe82000d3b192d890dabdfa0e8708d742a87",
"name": "browser-typecheck",
"command": [
"rtk",
"proxy",
"npm.cmd",
"exec",
"tsc",
"--",
"--project",
"../.artifacts/phase7/e2e-tsconfig.json",
"--noEmit"
],
"startedUtc": "2026-10-20T13:07:23.294714+00:00",
"finishedUtc": "2026-10-20T13:07:28.785676+00:00",
"exitCode": 0
},
"build": {
"path": ".artifacts/phase7/current/ui/build-command.json",
"sha256": "fd1802d73fe5c992f3e2adac0e0f35fe28221efbc4e3b7f61a793e6dcd5e70d6",
"name": "build",
"command": [
"rtk",
"proxy",
"npm.cmd",
"run",
"build"
],
"startedUtc": "2026-10-20T13:07:48.997996+00:00",
"finishedUtc": "2026-10-20T13:07:56.404637+00:00",
"exitCode": 0
},
"admin-users": {
"path": ".artifacts/phase7/current/ui/admin-users-command.json",
"sha256": "7fed5787b7d48faf52b0cf3a1fb65e0f8244177678f8738fa023cf50aefe6473",
"name": "admin-users",
"command": [
"rtk",
"proxy",
"node",
"node_modules/@playwright/test/cli.js",
"test",
"e2e/phase7-admin-users.spec.ts",
"--retries=0",
"--reporter=list,json",
"--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\admin-users-results"
],
"startedUtc": "2026-10-20T13:07:56.410189+00:00",
"finishedUtc": "2026-10-20T13:08:20.400526+00:00",
"exitCode": 0
}
},
"images": [
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-actual-get-recovery-en-light-390.png",
"sha256": "cbb227fbd4691b63f9f972ba868244456cc635895b3a5c9a2695f170e1449912"
},
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-long-identity-320-nb-dark.png",
"sha256": "42ebc78505ec0a0e937188b65bb23f6a15959a19ba307c4f9bce68b0a1b51249"
},
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-long-identity-390-en-light.png",
"sha256": "9d2d8c5c52f877c17d1e61cecfc9324865a02b00fde225768d9cca6cb470b26d"
},
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-long-identity-768-nb-light.png",
"sha256": "e7e47a6095da25a23902bf202e802f5f3ba89e4f8c7770b3a729b44ee5b681b1"
},
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-long-identity-1024-en-dark.png",
"sha256": "ac5b41db31027a8873c825a91363ad11114e674573606ad8b7a1a386165c19de"
},
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-long-identity-1440-en-light.png",
"sha256": "d80d2b1aa402d53429e1dc71c87a244cfb1d1af1d11fbf19916ab64523fb3c91"
},
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-long-identity-1920-nb-dark.png",
"sha256": "84a780ab684ebb9c6530e845bbc5cf70711d388881db12a4280af5214fdd05d1"
}
],
"scans": [
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-actual-get-recovery-en-light-390-axe.json",
"violations": 0,
"incompleteNodes": 3,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-long-identity-320-nb-dark-axe.json",
"violations": 0,
"incompleteNodes": 3,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-long-identity-390-en-light-axe.json",
"violations": 0,
"incompleteNodes": 3,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-long-identity-768-nb-light-axe.json",
"violations": 0,
"incompleteNodes": 4,
"incompleteRules": [
"aria-prohibited-attr",
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-long-identity-1024-en-dark-axe.json",
"violations": 0,
"incompleteNodes": 5,
"incompleteRules": [
"aria-prohibited-attr",
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-long-identity-1440-en-light-axe.json",
"violations": 0,
"incompleteNodes": 5,
"incompleteRules": [
"aria-prohibited-attr",
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-10/users-final-visuals/users-long-identity-1920-nb-dark-axe.json",
"violations": 0,
"incompleteNodes": 5,
"incompleteRules": [
"aria-prohibited-attr",
"color-contrast"
]
}
],
"frontend": {
"path": ".artifacts/phase7/current/unit/admin-users-final-20261020T130341Z-command.json",
"sha256": "4b5c069699c156d6a49fdc2b42cb0d6478f02950b1a1d6414bee0be1ba5157d8",
"command": [
"rtk",
"proxy",
"npm.cmd",
"test",
"--",
"--runInBand",
"--watchAll=false",
"--json",
"--outputFile=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\unit\\admin-users-final-20261020T130341Z.json",
"src/admin-users-page.test.tsx",
"src/confirm.test.tsx",
"src/confirm-flow.test.tsx",
"src/dialog-flow.test.tsx",
"src/modal-state.test.tsx"
],
"startedUtc": "2026-10-20T13:03:41.037736+00:00",
"finishedUtc": "2026-10-20T13:03:51.672970+00:00",
"exitCode": 0,
"counts": {
"numPassedTests": 18,
"numFailedTests": 0,
"numPendingTests": 0,
"numTodoTests": 0,
"numPassedTestSuites": 5,
"numFailedTestSuites": 0
}
},
"backend": {
"path": ".artifacts/phase7/current/backend-career/p710-users-entry-20261020T125635Z/command.json",
"sha256": "70d503a6ff1c27f6b1361d388020ad13406c87b5e8079b96a428dddd436d3b2f",
"command": [
"rtk",
"proxy",
"C:/Users/Connor/.dotnet/dotnet.exe",
"test",
"JobTrackerApi.Tests/JobTrackerApi.Tests.csproj",
"--configuration",
"Release",
"--no-build",
"--filter",
"FullyQualifiedName~UsersControllerTests|FullyQualifiedName~ApiAuthorizationPostureTests|FullyQualifiedName~SessionContextMiddlewareTests",
"--logger",
"trx;LogFileName=career.trx",
"--results-directory",
"E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\backend-career\\p710-users-entry-20261020T125635Z"
],
"startedUtc": "2026-10-20T12:56:35.309094+00:00",
"finishedUtc": "2026-10-20T12:56:38.915911+00:00",
"exitCode": 0,
"logSha256": "d9e8e89d9b46ba4575d4f2fd52a63ff317897e698356cd499dad7d8616c893f5",
"counts": {
"total": 25,
"passed": 25,
"failed": 0,
"notExecuted": 0
},
"resultSha256": "677e93e67549b1076d5a0dc804d8f0c86779d1a845cd09c79b4958edd8afc7ef"
},
"buildId": "f6H1YeLZWstdE-yoN5W8B",
"nativeStats": {
"startTime": "2026-10-20T13:07:56.934Z",
"duration": 23435.509000000002,
"expected": 3,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"nativeResultSha256": "7afbc369f3a9e628074fefaf75eab31c45139adba59f03d78687ec74541c3654"
}
@@ -0,0 +1,104 @@
{
"schemaVersion": 1,
"checkpoint": "p7-10",
"status": "PASS",
"checkedUtc": "2026-10-20T13:09:54.281127+00:00",
"commands": [
{
"name": "secret-scan",
"command": [
"rtk",
"proxy",
"C:\\Users\\Connor\\AppData\\Local\\Programs\\Python\\Python312\\python.exe",
"-X",
"utf8",
"scripts/supply-chain.py",
"scan-secrets"
],
"startedUtc": "2026-10-20T13:09:50.288246+00:00",
"finishedUtc": "2026-10-20T13:09:53.446671+00:00",
"exitCode": 0,
"log": ".artifacts/phase7/current/static/20261020T130950Z/secret-scan.log",
"logSha256Raw": "be5119e6057287d397e87759f855c91e634e3bfac61c8be7584d05592ba2dfcd"
},
{
"name": "working-whitespace",
"command": [
"rtk",
"proxy",
"git",
"diff",
"--check"
],
"startedUtc": "2026-10-20T13:09:53.447162+00:00",
"finishedUtc": "2026-10-20T13:09:53.545286+00:00",
"exitCode": 0,
"log": ".artifacts/phase7/current/static/20261020T130950Z/working-whitespace.log",
"logSha256Raw": "fc361bb530dd5610a4744d4fd10476a3ff92609526be469ce0c32eb4ac9ea3bb"
},
{
"name": "staged-whitespace",
"command": [
"rtk",
"proxy",
"git",
"diff",
"--cached",
"--check"
],
"startedUtc": "2026-10-20T13:09:53.545789+00:00",
"finishedUtc": "2026-10-20T13:09:53.621382+00:00",
"exitCode": 0,
"log": ".artifacts/phase7/current/static/20261020T130950Z/staged-whitespace.log",
"logSha256Raw": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"name": "phase7-whitespace",
"command": [
"rtk",
"proxy",
"git",
"diff",
"--check",
"b5319981ad3d1bd635daff861453d23c0de5320b"
],
"startedUtc": "2026-10-20T13:09:53.621382+00:00",
"finishedUtc": "2026-10-20T13:09:53.864111+00:00",
"exitCode": 0,
"log": ".artifacts/phase7/current/static/20261020T130950Z/phase7-whitespace.log",
"logSha256Raw": "296a8d67450c089ffaa00fc6935bdd1b2009396bc7edd24a6f693de0fd20e51e"
}
],
"untrackedSecretScan": {
"files": 4,
"findings": 0
},
"documentation": {
"files": 32,
"relativeLinks": 626,
"errors": []
},
"preservation": {
"groups": 57,
"statuses": {
"PARTIAL": 56,
"PASS": 0,
"FAIL": 0,
"DEFERRED": 1
},
"originalRoutesPreserved": true,
"currentConcreteRoutes": 33
},
"production": {
"main": "cc7977131a4f991578f29d83eec17a242a858fca",
"originMain": "cc7977131a4f991578f29d83eec17a242a858fca",
"status": "NO-GO",
"qualification": "Local fetched refs; direct push observation recorded separately."
},
"hashPolicy": "Portable JSON UTF8 LF; raw log hashes measure exact saved bytes.",
"limits": [
"Pattern scanner skips binary and files over2MiB; not historical Git/production secrets.",
"Local links check target existence, not URL/heading anchors.",
"Static evidence is not runtime/manual/device/provider/production acceptance."
]
}