Files
jobtrackingapp/docs/redesign/phase-8/p8-03/BACKEND.md
T

8.7 KiB

P8-03 backend persistence evidence

Verified locally on 21 October 2026 against the Phase 7 WAL baseline. No production database or external service was used.

Contract and ownership

GET and PUT /api/auth/preferences/dashboard require the existing local authentication scheme. The principal supplies the account ID; query/body account IDs have no authority. Missing/unknown subjects return 401. DTO: {version:1, revision, widgets:[{id,visible,size}]}. No saved layout returns revision 0 and an empty widget array. Successful PUT returns the canonical DTO with revision incremented by one; a stale revision returns 409 with error: dashboard_revision_conflict.

Writes accept at most 16,384 actual UTF-8 bytes, including chunked requests; larger bodies return 413. Invalid JSON, unsupported/missing version, non-integer/negative/exhausted revision, absent widgets, more than 32 entries, unknown/duplicate IDs, absent/non-boolean visibility, and invalid sizes return 400 without saving. Incoming revision must be below 9,007,199,254,740,991 so the incremented revision remains an exact JavaScript integer. Supported IDs are the nine in the Phase 8 catalogue. cards accepts only full; every other widget accepts standard or full. Unsupported sizes are rejected on writes. Only validated canonical fields are stored; extra request fields are discarded. An empty saved list is valid, but has a positive revision and therefore remains distinct from an unsaved account. The frontend owns missing-widget defaults and explicit reset behaviour.

Reads retain the database revision. Unknown, duplicate or malformed saved widgets are omitted; unsupported saved sizes become full for cards and standard otherwise. Corrupt or oversized stored JSON becomes an empty list without resetting the revision. This permits a deliberate revision-guarded repair save and prevents corrupt storage from rendering arbitrary content. A bounded JSON document declaring an integer schema version greater than 1 instead exposes that actual version and an empty widget list. PUT rejects such stored future schemas with 409 dashboard_version_unsupported, even when a version-1 request supplies the matching revision; newer settings remain intact and the frontend can disable editing.

ApplicationUser owns nullable DashboardLayoutJson and default-zero DashboardLayoutRevision. The endpoint uses one SQL ExecuteUpdateAsync guarded by owner ID and expected revision; it updates exactly these two columns. Simultaneous first writes cannot both win. No layout write invokes UserManager.UpdateAsync.

A reproduced additional race showed that an unrelated stale Identity whole-user update could otherwise replay the old layout/revision. Both layout properties ignore ordinary EF tracked updates after insertion. Their conditional endpoint update remains effective. The regression marks every property modified, matching Identity's store behaviour, then proves the newer layout survives. Profile, language, notification settings and Identity stamps remain unchanged by layout saves.

Migration and startup

Additive migration 20261021090000_AddDashboardLayoutPreference adds nullable TEXT/longtext JSON and non-null INTEGER/bigint revision default 0. No new table or backfill of layout choices. The snapshot includes both properties. SQLite and MariaDB startup reconciliation intentionally leave these columns to the migration, matching the existing language preference owner; adding them before the per-migration loop would collide on fresh startup. Actual fresh and legacy file startup plus existing complete migration-chain and repeated-startup tests pass.

The explicit SQLite downgrade drops only these independent columns using native SQLite DROP COLUMN (requires SQLite 3.35+, satisfied by the tested bundled runtime). MariaDB uses normal EF DropColumn operations. Migration-chain downgrade/upgrade tests pass; downgrade necessarily removes saved layouts. No downgrade was performed outside disposable tests.

Test evidence

All commands used the pinned executable C:\Users\Connor\.dotnet\dotnet.exe through rtk, project JobTrackerApi.Tests/JobTrackerApi.Tests.csproj, flags --configuration Release --no-restore, and a TRX logger/results directory below .artifacts/phase8/.

Run Filter Actual result TRX
Meaningful pre-implementation RED FullyQualifiedName~DashboardPreferencesTests 0 pass / 13 fail / 0 skipped; absent routes/model backend-red/dashboard-red.trx
Initial implementation same 13 pass / 0 fail / 0 skipped backend-green/dashboard-green.trx
Stale Identity race RED FullyQualifiedName~Stale_identity_profile_update 0 pass / 1 fail; saved revision 1 overwritten by 0 backend-stale-red/dashboard-stale-red.trx
First expanded run dashboard + migration chain + SQLite startup + schema ownership 25 pass / 16 fail; 15 downgrade SQL generation failures and one malformed fixture SQL backend-focused/dashboard-focused.trx
Downgrade fix successor same 40 pass / 1 fail; remaining malformed fixture SQL backend-focused-successor/dashboard-focused.trx
Final expanded run same, plus two further dashboard HTTP cases 43 pass / 0 fail / 0 skipped backend-focused-final/dashboard-focused.trx
Complete backend no filter 1081 pass / 0 fail / 0 skipped, 29 seconds backend-full/dashboard-full.trx
Future-schema / JS-safe revision RED FullyQualifiedName~Future_schema|FullyQualifiedName~Invalid_payload_does_not_save 12 pass / 2 fail; future version hidden and unsafe exhausted revision accepted backend-future-red/dashboard-future-red.trx
Complete backend after future-schema repair no filter 1083 pass / 0 fail / 0 skipped, 32 seconds backend-full-final/dashboard-full.trx

The initial test-harness attempt omitted historical startup reconciliation and failed before reaching endpoints (missing historical AvatarImageDataUrl column). The meaningful RED above reran after wiring the actual startup owner. The subsequent malformed fixture SQL used a literal brace through EF's formatting API; parameterising it corrected the fixture without changing the product expectation.

Final focused command:

rtk C:\Users\Connor\.dotnet\dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter "FullyQualifiedName~DashboardPreferencesTests|FullyQualifiedName~MigrationChainTests|FullyQualifiedName~SqliteStartupConcurrencyTests|FullyQualifiedName~StartupSchemaOwnershipTests" --logger "trx;LogFileName=dashboard-focused.trx" --results-directory .artifacts/phase8/backend-focused-final

Final complete command:

rtk C:\Users\Connor\.dotnet\dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --logger "trx;LogFileName=dashboard-full.trx" --results-directory .artifacts/phase8/backend-full-final

The 23 new dashboard cases use an actual local Kestrel HTTP listener, real EF SQLite files, the complete migrations and actual startup reconciliation. They verify 401, owner isolation despite an attempted query account override, GET/PUT acknowledgement, 409, concurrent first saves, unchanged unrelated account fields/stamps, stale Identity preservation, invalid/oversized/chunked payloads, all catalogue IDs/order, empty saved lists, corrupt reads, future-schema downgrade protection, exact JavaScript revision bounds, legacy defaults and provider SQL generation. They also hold a SELECT reader open while saving the actual layout against the WAL file.

Limits

The HTTP fixture substitutes a test-only local authentication handler; it proves endpoint scheme/owner enforcement, not production cookie issuance, CSRF/session expiry or proxy behaviour. It uses the existing minimal EF host conventions; TestHostFactory has no reusable real HTTP host, so the fixture adds only the services needed by AuthController/startup. Existing auth tests remain in the passing complete suite. Browser/session integration belongs to the UI acceptance lane.

MariaDB migration SQL is generated with the installed Pomelo provider and explicitly checks longtext NULL and bigint NOT NULL DEFAULT 0. This is SQL inspection, not MariaDB runtime evidence. rtk docker ps --format "{{.Names}} {{.Image}} {{.Ports}}" failed because the Docker Desktop Linux engine pipe was unavailable; no disposable MariaDB instance was exercised or created.

The held-reader and simultaneous-save tests establish finite regression evidence. WAL does not eliminate every form of writer contention; no retries, swallowed provider failures or permanent reliability claim were introduced. Human/device, deployed configuration, backup/restore and production gates remain outstanding; production remains NO-GO.