WIP: User-settings foundation (store + registry + gRPC) via show_empty_projects #290

Closed
toasterson wants to merge 1 commit from claude/wi-019f17e4-user-settings-foundation-store-registry into main
Owner

Anima work item 019f17e4-cb7e-71a0-bafc-1d85d2f69215.

What to build

The whole user-settings subsystem end-to-end, proven with ONE real setting — show_empty_projects. A user toggles "Show empty projects" on the Settings page → it persists server-side → survives reload and follows them to another device/browser → and actually filters the projects list. This is the tracer bullet that builds the skeleton every future setting (incl. the inbox window) slots into.

End-to-end behaviour: Settings → Preferences loads the user's settings from the server (stored values merged over registry defaults); flipping "Show empty projects" writes to the server; reloading keeps the value; projects with zero work items are hidden when the toggle is off.

Pushed context

Design is fixed by ADR 0016 (docs/adr/0016-user-settings-model.md) and the User settings entry in CONTEXT.md — read both first. The four decisions:

  1. Server-authoritative, account-level (cross-device) — NOT localStorage.
  2. Hybrid storage: values in a KV/JSON store, but a typed settings registry in code is the source of truth for which keys exist + their value type + default + validation. Adding a setting = one registry entry, no migration.
  3. User-global scope, key (user_id, key).
  4. gRPC, self-scoped: a user reads/writes only their own settings; no MCP, no admin-edits-others.

Build it vertically (one cut through all layers — do NOT split backend/frontend):

  • Schema: new migration user_settings (user_id uuid, key text, value jsonb, updated_at timestamptz, PRIMARY KEY (user_id, key)), FK user_id → users. Follow the existing migration numbering/style in crates/anima-db/migrations/ (next sequential number; never edit an applied migration).
  • Registry: a new module in crates/anima-core/src/domain/ (e.g. user_setting.rs) — an enum/registry of valid keys with typed value + default + validation. Seed ONE entry now: show_empty_projects (bool, default true). Keep it the single source of truth (the GetUserSettings merge and SetUserSetting validation both go through it).
  • Repo: SettingsRepo in crates/anima-db/src/repo/get_all(user_id) (returns stored values), get_value(user_id, key) (value-or-registry-default, for server-internal reads), set(user_id, key, value) (upsert). Validation against the registry happens in the service layer.
  • gRPC: new proto/anima/v1/settings.protoSettingsService { GetUserSettings, SetUserSetting }; GetUserSettings returns the full merged set (every registry key with the user's value or its default), SetUserSetting(key, value) validates against the registry and rejects unknown keys / bad types. Resolve user_id from the authenticated identity (see how crates/anima-server/src/services/ services + crate::auth get the caller); reject anonymous. Register the service in the server bootstrap. Regenerate Rust + PWA stubs (npm run generate in anima-pwa/, commit src/gen/* — stale gen masks drift).
  • PWA: anima-pwa/src/lib/grpcApi.ts — add getUserSettings() / setUserSetting(key, value) with mock fallbacks in mockApi.ts (+ mockData.ts fixtures). anima-pwa/src/pages/Settings/Settings.tsx — replace the fake useState(showEmptyProjects) (lines ~13, 57-61) with a load-from-getUserSettings + write-via-setUserSetting flow. Consume the value where projects are listed (the Projects page / sidebar) to actually hide zero-work-item projects when off.
  • This is user settings, not project config — don't touch Project/Plan executor/workflow fields.

Acceptance criteria

  • user_settings migration applies cleanly; (user_id, key) PK; values are JSONB.
  • Settings registry module exists with show_empty_projects (bool, default true) as the sole entry; unknown keys / wrong types are rejected by SetUserSetting.
  • SettingsService GetUserSettings/SetUserSetting are self-scoped (a user cannot read or write another user's settings); anonymous is rejected.
  • PWA: toggling "Show empty projects" persists across reload (server round-trip, not local state); projects with no work items are hidden when off.
  • Rust + PWA stubs regenerated/committed; cargo check -p anima-core -p anima-db -p anima-server clean; npm run verify green (tests: repo default-vs-set, service self-scope rejection, PWA persists-across-reload).

Blocked by

  • None — can start immediately.
Anima work item `019f17e4-cb7e-71a0-bafc-1d85d2f69215`. ## What to build The whole user-settings subsystem end-to-end, proven with ONE real setting — `show_empty_projects`. A user toggles "Show empty projects" on the Settings page → it persists server-side → survives reload and follows them to another device/browser → and actually filters the projects list. This is the tracer bullet that builds the skeleton every future setting (incl. the inbox window) slots into. End-to-end behaviour: Settings → Preferences loads the user's settings from the server (stored values merged over registry defaults); flipping "Show empty projects" writes to the server; reloading keeps the value; projects with zero work items are hidden when the toggle is off. ## Pushed context Design is fixed by **ADR 0016** (`docs/adr/0016-user-settings-model.md`) and the **User settings** entry in `CONTEXT.md` — read both first. The four decisions: 1. **Server-authoritative**, account-level (cross-device) — NOT localStorage. 2. **Hybrid storage**: values in a KV/JSON store, but a typed **settings registry in code** is the source of truth for which keys exist + their value type + default + validation. Adding a setting = one registry entry, no migration. 3. **User-global** scope, key `(user_id, key)`. 4. **gRPC, self-scoped**: a user reads/writes only their own settings; **no MCP**, **no admin-edits-others**. Build it vertically (one cut through all layers — do NOT split backend/frontend): - **Schema:** new migration `user_settings (user_id uuid, key text, value jsonb, updated_at timestamptz, PRIMARY KEY (user_id, key))`, FK `user_id → users`. Follow the existing migration numbering/style in `crates/anima-db/migrations/` (next sequential number; never edit an applied migration). - **Registry:** a new module in `crates/anima-core/src/domain/` (e.g. `user_setting.rs`) — an enum/registry of valid keys with typed value + default + validation. Seed ONE entry now: `show_empty_projects` (bool, default `true`). Keep it the single source of truth (the `GetUserSettings` merge and `SetUserSetting` validation both go through it). - **Repo:** `SettingsRepo` in `crates/anima-db/src/repo/` — `get_all(user_id)` (returns stored values), `get_value(user_id, key)` (value-or-registry-default, for server-internal reads), `set(user_id, key, value)` (upsert). Validation against the registry happens in the service layer. - **gRPC:** new `proto/anima/v1/settings.proto` — `SettingsService { GetUserSettings, SetUserSetting }`; `GetUserSettings` returns the full merged set (every registry key with the user's value or its default), `SetUserSetting(key, value)` validates against the registry and rejects unknown keys / bad types. Resolve `user_id` from the authenticated identity (see how `crates/anima-server/src/services/` services + `crate::auth` get the caller); reject anonymous. Register the service in the server bootstrap. Regenerate Rust + PWA stubs (`npm run generate` in `anima-pwa/`, commit `src/gen/*` — stale gen masks drift). - **PWA:** `anima-pwa/src/lib/grpcApi.ts` — add `getUserSettings()` / `setUserSetting(key, value)` with mock fallbacks in `mockApi.ts` (+ `mockData.ts` fixtures). `anima-pwa/src/pages/Settings/Settings.tsx` — replace the fake `useState(showEmptyProjects)` (lines ~13, 57-61) with a load-from-`getUserSettings` + write-via-`setUserSetting` flow. Consume the value where projects are listed (the Projects page / sidebar) to actually hide zero-work-item projects when off. - This is **user settings**, not project config — don't touch Project/Plan executor/workflow fields. ## Acceptance criteria - [ ] `user_settings` migration applies cleanly; `(user_id, key)` PK; values are JSONB. - [ ] Settings registry module exists with `show_empty_projects` (bool, default true) as the sole entry; unknown keys / wrong types are rejected by `SetUserSetting`. - [ ] `SettingsService` GetUserSettings/SetUserSetting are self-scoped (a user cannot read or write another user's settings); anonymous is rejected. - [ ] PWA: toggling "Show empty projects" persists across reload (server round-trip, not local state); projects with no work items are hidden when off. - [ ] Rust + PWA stubs regenerated/committed; `cargo check -p anima-core -p anima-db -p anima-server` clean; `npm run verify` green (tests: repo default-vs-set, service self-scope rejection, PWA persists-across-reload). ## Blocked by - None — can start immediately.
toasterson force-pushed claude/wi-019f17e4-user-settings-foundation-store-registry from 86cb1842e0 to 8361a31114 2026-07-05 01:52:51 +00:00 Compare
toasterson force-pushed claude/wi-019f17e4-user-settings-foundation-store-registry from 8361a31114 to 8d938c9c22 2026-07-06 07:02:53 +00:00 Compare
toasterson force-pushed claude/wi-019f17e4-user-settings-foundation-store-registry from 8d938c9c22 to 43e92b2efe 2026-07-06 07:40:16 +00:00 Compare
toasterson force-pushed claude/wi-019f17e4-user-settings-foundation-store-registry from 43e92b2efe to d78913dfcc 2026-07-07 21:50:23 +00:00 Compare
toasterson force-pushed claude/wi-019f17e4-user-settings-foundation-store-registry from d78913dfcc to e266817829 2026-07-11 12:44:25 +00:00 Compare
Author
Owner

Closing as superseded by main.

main now carries a strictly more capable SettingsService than this branch proposes:

this branch main today
RPCs GetUserSettings, SetUserSetting GetSettings, SetSetting, ListSettingDefs
scope user only user and project (user_project-scoped view settings, ADR 0020)
registry none SettingDef with default_json + description
value type google.protobuf.Value value_json

The settings store is also already load-bearing in production — dispatch_enabled is a row in settings, and ProjectServiceImpl::attach_settings reads per-caller effective settings on every project listing.

Rebasing this branch is not a merge: proto/anima/v1/settings.proto, crates/anima-server/src/services/settings.rs and crates/anima-db/src/repo/settings.rs all conflict add/add, i.e. two independent implementations of the same feature. Porting the branch onto main's API would be a re-implementation with no remaining delta.

Work item WI-264 is being cancelled as delivered-by-other-means. If any part of the user-scoped design is still wanted, it should be filed fresh against main's SettingsService rather than resurrected here.

Context: 2026-07-28 sweep of the 11 open PRs that could no longer rebase onto main. 12 branches were rebased and landed; this is one of three left for a decision.

Closing as **superseded by `main`**. `main` now carries a strictly more capable `SettingsService` than this branch proposes: | | this branch | `main` today | |---|---|---| | RPCs | `GetUserSettings`, `SetUserSetting` | `GetSettings`, `SetSetting`, `ListSettingDefs` | | scope | user only | user **and** project (`user_project`-scoped view settings, ADR 0020) | | registry | none | `SettingDef` with `default_json` + `description` | | value type | `google.protobuf.Value` | `value_json` | The settings store is also already load-bearing in production — `dispatch_enabled` is a row in `settings`, and `ProjectServiceImpl::attach_settings` reads per-caller effective settings on every project listing. Rebasing this branch is not a merge: `proto/anima/v1/settings.proto`, `crates/anima-server/src/services/settings.rs` and `crates/anima-db/src/repo/settings.rs` all conflict `add/add`, i.e. two independent implementations of the same feature. Porting the branch onto `main`'s API would be a re-implementation with no remaining delta. Work item WI-264 is being cancelled as delivered-by-other-means. If any part of the user-scoped design is still wanted, it should be filed fresh against `main`'s `SettingsService` rather than resurrected here. Context: 2026-07-28 sweep of the 11 open PRs that could no longer rebase onto `main`. 12 branches were rebased and landed; this is one of three left for a decision.
toasterson closed this pull request 2026-07-28 08:44:01 +00:00

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
toasterson/Anima!290
No description provided.