mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-10-25 21:20:25 +00:00
e7b457f456
* feat(db): per-user preference store with versioned revision contract Add the user_preferences table (per user and domain, with a strictly advancing integer revision and schema_version) plus the atomic persistence methods behind it: - getUserPreferenceDomain returns a transport envelope with corrupt-row reporting (the revision stays intact so clients can repair) - mutateUserPreferenceDomain applies a document mutation inside a single transaction under an expectedRevision or create-if-absent precondition; two competing writers cannot both succeed - resetUserPreferenceDomain writes a tombstone (schema_version 0) under the same precondition contract, so a post-reset edit can conditionally un-tombstone while a stale pre-reset edit conflicts - deleteUserPreferenceDomains removes every row on account deletion (foreign keys are declarative only in this codebase, so deleteUser runs the cleanup explicitly in its transaction) * feat(preferences): per-user preference API with conditional writes Add the /api/user-preferences router: collection GET, per-domain PUT and DELETE (reset to tombstone), and a migrate endpoint for the create-if-absent first-run race. - Every operation is identity-guarded: the x-sencho-pref-user header must match the authenticated user before any persistence access, so a stale tab can never read or write another account's rows. A mismatch is a 409 IDENTITY_CHANGED with no current envelope, kept distinct from the revision-conflict 409 CONFLICT which carries one. - Writes are conditional (expectedRevision or absent: true); there is no unguarded write path. Stale preconditions return the fresh envelope so clients reconcile instead of retrying blind. - Documents are validated server-side with strict Zod schemas mirroring the frontend field guards; legacy navigation mode values are rejected - Machine identities (node proxy, pilot tunnel) are rejected and API tokens lack the required scope - The route is hub-only: a remote x-node-id targeting it gets an explicit 403, since account-owned state lives on the hub - Audit summaries cover migrate, update, reset, and failed attempts * feat(preferences): parity script pinning server enums to frontend registries The server-side enum tables duplicate the frontend's field guards, and nothing in the TypeScript compilation boundary links the two (backend and frontend compile separately). Add a standalone check script that extracts both value sets from source text and fails on drift, wired as the pretest step for backend and frontend so a mismatch surfaces in the ordinary test run instead of as a 400 at write time. * feat(preferences): frontend preference domains, sync bus, and identity guards Introduce the client half of server-backed per-user preferences: the existing localStorage hooks remain the owners of state and DOM application; a thin sync layer carries their documents to and from the per-user preference API. - preferenceEvents: the leaf module with write notifications (dirty field attribution), the identity-generation counter, the eligibility-readiness store with ownership-captured publications, and the unsaved-episode failure surface - preferencesDocuments: build and per-field sanitize of appearance and navigation documents, tombstone-aware hydration, and the owner marker that wipes cached values when another account claims the browser - syncBus: the single write choke point. Per-domain serialization, same-kind coalescing, reset-cancels-prior-puts chronology, debounced trailing writes, conditional PUTs with 409 reconciliation (reset conflicts re-queue the DELETE once on the adopted revision), keepalive flush on page hide, and pump-level identity guards that drop operations captured by a superseded account or generation - useUserPreferencesSync: the always-mounted sync owner. Hydrates on identity resolution, reconciles late GETs against dirty fields (server wins untouched fields), adopts remote resets by observable revision, repairs corrupt rows with a conditional PUT, and surfaces failures as a single unsaved episode per domain - AuthContext: sequenced identity transitions and a cross-tab storage signal so a login or logout in one tab re-checks auth in the others - the localStorage hooks gain apply/current exports for the hydration path; user-facing setters notify the bus, hydration and derived seeding paths deliberately do not * feat(preferences): reset actions, account-scope copy, and frontend tests Wire the per-domain reset actions into the settings surface: the appearance section footer and navigation section each offer a complete-domain reset, and the recovery section's interface reset runs both domains through the shared handler with a reload only after both tombstones settle successfully. - Settings registry: appearance scope moves from browser to account, with copy that states where preferences now live - ThemeQuickSwitch: the save-location line reflects account syncing - Tests: dirty-field merge against a late hydration GET, identity guards (cache ownership, queue invalidation on account switch, loading-state no-op), corrupt-row repair through the hook path, remote-reset precedence, reset settle/failure handling, registry scope assertions, and the eligibility readiness bridge * test(e2e): cross-browser preference convergence suite Add the end-to-end spec covering the promise the feature makes: two browser contexts on the same account converge (appearance fields, density, navigation mode and pin order), a reset in one context reaches the other on reload, an account switch renders the new account's state rather than the previous account's cache, and a pending write captured before the switch is rejected server-side without touching either account's rows. The existing navigation, motion, and effects suites seed a known server-side preference baseline per suite account so the new hydration layer cannot overwrite their localStorage-only setups. * docs: appearance and navigation preferences are account-scoped Rewrite the user-facing pages that described browser-only personalization: the appearance feature page (including the quick switcher, troubleshooting entries, and screenshot description), the settings reference scope legend, and the multi-node and overview pages that stated preferences stay per browser. The recovery page's emergency-reset entry now explains that clearing site data only drops the local cache, not the saved preferences. * test(preferences): mint API tokens through the shared test helper Replace the duplicated token-minting code with the existing createTestApiToken helper so the file leans on the repo-standard scaffolding and CodeQL stops flagging the inline sha256 hashing as new alerts on this diff.