shipped (E2E pending)

Spec: F75 — Settings split (Organization Admin vs Personal Settings)

Status: shipped (E2E pending)

1. Problem & CEO Job

See clearly / trust the signal. The sidebar puts Teams, Invite, Settings, Sources, Support, and Referral in one owner/admin-only Admin group. This causes two failures:

  1. Ordinary members have no settings. Sidebar.tsx gates the whole group on isAdminOrOwner, so members cannot reach Support, Referral, notification preferences, language, or their own password.
  2. Org and personal controls are conflated. “Connected Sources” (org-wide) sits beside “Referral” (personal), obscuring whether a change affects the company or only the user.

This feature creates Organization (admin/owner: Teams, Invite Members, Connected Sources, Organization Settings) and Personal (every member: Account, Support, Referral) groups, plus a personal Account page for name, avatar, password, language, and notification preferences.

2. User Stories & Acceptance Criteria

  • As a member (non-admin), I want a Personal menu I can always see, so that I can manage my own account, get support, and use the referral program without admin rights.
    • Given a member role, when the sidebar renders, then a Personal group is visible with Account, Support, and Referral; and no Organization group is shown. — Sidebar.tsx; Sidebar.test.tsx 14/14 (§11 row 5 ✅)
    • Given a member role, when they open /settings/account, then they can edit their name, upload an avatar, change their password, set their language, and toggle their personal notification preferences. — AccountPage.tsx; authApi.updateProfile/changePassword; AccountPage.test.tsx 4/4 (§11 row 6 ✅)
  • As an owner/admin, I want organization controls grouped apart from my personal account, so that I never confuse a company-wide change with a personal one.
    • Given an owner/admin role, when the sidebar renders, then an Organization group shows Teams, Invite Members, Connected Sources, and Organization Settings, and a Personal group shows Account, Support, Referral. — Sidebar.tsx; Sidebar.test.tsx 14/14 (§11 row 5 ✅)
    • Given an owner/admin, when they open Organization Settings, then they see org name, billing, switch/create/join org, and the danger zone (current SettingsPage content). — SettingsPage.tsx relabelled; notif prefs moved to Account (§11 row 7 ✅)
  • As any authenticated user, I want my password change to require my current password, so that a hijacked session can’t silently lock me out.
    • Given a wrong current password, when changing password, then 400 and no change. — ChangePasswordSerializer; TestChangePassword (§11 row 2 ✅)
    • Given a member of org A, when they call any account endpoint, then only their own user row is read/written — never another user’s (no organization scope leak; the endpoint is request.user-scoped, not tenant-scoped). — TestAccountSelfScope in test_multitenancy.py (§11 row 3 ✅)

Acceptance criteria must be testable — each maps to at least one test in §7.

3. Scope Boundaries

  • In:
    • Sidebar information architecture: replace the single Admin group with two groups — Organization (role-gated) and Personal (all members).
    • New personal Account page (/settings/account) and its backend: update own name + avatar_url, change password, plus the already-existing language and personal notification-preference surfaces re-homed here.
    • Re-home existing pages without changing their internals: Teams, Invite, Sources, Support, Referral, and the existing org SettingsPage (renamed Organization Settings).
  • Out:
    • Any change to Teams (F03), Invite/members (F02), Connected Sources (F33/F44), Support (F58), Referral (F59), Billing (F24) internals — this feature only re-groups and re-labels them.
    • Avatar storage mechanics — avatar upload reuses F34 pre-signed S3 POST; this spec only wires the resulting URL onto the user.
    • Role/permission model changes (F02) — gating uses the existing myRole derivation.
    • Fine-grained admin sub-roles, per-page permissions — out (see MVP_SCOPE “Advanced RBAC”).

4. Data Model

No new models. Uses existing accounts.User:

  • User.name — editable via new self-update endpoint.
  • User.avatar_url (URLField, already present) — set from an F34 upload result.
  • User.language / language_detected — already editable via /auth/me/language/ (F37).
  • Password — AbstractBaseUser hashed password; changed via new endpoint (validates current password, runs Django password validators).
  • NotificationPreferences (F35/F36, /notification-preferences/me/) — already per-user; surfaced on the Account page.

No organization FK is involved — account endpoints are scoped to request.user, not request.tenant. This is the one deliberate exception to the tenant-scoping rule (Rule 1): a user is a global identity that belongs to many orgs, so their account/profile is not tenant data. Org membership/role is unchanged (F02).

5. API Contract

MethodPathAuth/RoleRequestResponseErrors
PATCH/api/auth/me/authenticated (self){name?, avatar_url?}UserSerializer400 invalid name/url
POST/api/auth/me/change-password/authenticated (self){current_password, new_password}204400 wrong current / weak new
PATCH/api/auth/me/language/authenticated (self){language}{language}400 unsupported (existing, F37)
GET/PATCH/api/notification-preferences/me/authenticated (self)prefsprefs(existing, F35)
POST/api/uploads/presign/authenticatedfilename, type, sizepresigned POST(existing, F34 — avatar upload)

Scoping: account endpoints resolve the row as request.user (self), never by id from the path and never by organization. There is no cross-tenant query to scope because no org data is touched. change-password re-checks request.user.check_password(current_password) and runs django.contrib.auth.password_validation.validate_password(new_password, user) before saving; existing sessions/tokens for the user remain valid (no forced logout in MVP — see §12).

No AI output → no EvidenceSource (n/a).

6. UI / UX

Sidebar (frontend/src/components/Sidebar.tsx) — replace the single group_admin block:

ORGANIZATION            (only if isAdminOrOwner)
  Teams                 /settings/teams     Users
  Invite Members        /settings/invite    UserPlus
  Connected Sources     /settings/sources   Plug
  Organization Settings /settings           Building2

PERSONAL                (always, for every authenticated member)
  Account               /settings/account   Settings
  Support               /support      Headphones
  Referral              /settings/referrals  Gift
  • Connected Sources keeps its existing dev-only gate (/settings/sources ∈ DEV_ONLY_PATHS).
  • The group_personal block is rendered outside the isAdminOrOwner conditional so members see it.

New page — AccountPage.tsx at /settings/account (account namespace):

  • Sections (shadcn Card): Profile (name + avatar upload), Security (change password: current + new + confirm, via useZodForm), Language (existing selector, re-homed), Notifications (the personal NotificationPreferences toggles moved out of SettingsPage).
  • States: loading skeleton (while me resolves), saving (button disabled + label), success/error toasts, password mismatch / weak-password inline FormMessage, avatar-upload error toast.
  • shadcn only: Card, Input, Label, Button, Switch, Form*, Avatar (check frontend/src/components/ui/ — add avatar via shadcn if absent).

SettingsPage.tsx → relabel as “Organization Settings” — move the personal NotificationPreferences block to AccountPage; keep org name, billing, org-access, switch, and danger zone. Update t('title') to the org-settings heading.

i18n: new account namespace (en + vi together) — title, profile_*, name_label, avatar_*, security_title, current_password, new_password, confirm_password, password_changed, password_error, language_*, plus the moved notifications_* keys. New sidebar keys: group_organization, group_personal, account, org_settings (replacing settings/group_admin usage where the label changes). DESIGN.md sidebar + Card surfaces; TASTE.md pre-flight applies.

7. Test Plan

#CaseTypeWhere
1PATCH /auth/me/ updates own name + avatar_urlbackendbackend/tests/test_api.py::TestAccountSelfUpdate
1aMobile profile image picker validates format/size, completes F34 upload, then persists own avatar_urlshared/nativeandroid/shared/src/commonTest/kotlin/com/locuno/lumi/shared/presentation/settings/SettingsScreenModelTest.kt · android/app/src/test/kotlin/com/locuno/lumi/android/ui/SettingsAvatarSourceTest.kt
2Change password: correct current → 204; wrong current → 400; weak new → 400backendbackend/tests/test_api.py::TestChangePassword
3Account endpoints touch only request.user, never another user’s row (no cross-user write)backendbackend/tests/test_multitenancy.py::TestAccountSelfScope
4Member (non-admin) sees Personal group, not Organization group; admin sees bothfrontendfrontend/src/test/Sidebar.test.tsx
5Account page: edit name, change password, toggle notif renders + submitsfrontendfrontend/src/test/AccountPage.test.tsx (MSW)
6E2E: member logs in → opens /settings/account → changes password; admin sees Organization groupe2efrontend/tests/e2e/lumi.spec.ts describe("Settings split")

Backend coverage ≥99%; new E2E describe block required.

8. Current Behavior

Sidebar.tsx renders Organization and Personal groups with role-appropriate access. The web AccountPage.tsx supports self-scoped name, avatar, password, language, and notification controls; the shared mobile Settings surface now provides the same name/password/avatar contract on Android and iOS through the F34 upload pipeline. The native picker/runtime tour remains an audit proof gate, but avatar upload is no longer a missing product path. /api/auth/me/ remains self-scoped: it accepts name and avatar_url only for the authenticated user.

9. Error States

#ErrorResponseNotes
ES1Change password, wrong current400 {"current_password": ["Incorrect password."]} → toast account.password_errorTest #2
ES2New password fails Django validators400 {"new_password": [...]} → inline FormMessageTest #2
ES3New ≠ confirmclient-side Zod refine, no requestaccount.password_mismatch
ES4Avatar upload fails (F34 presign/PUT)toast account.avatar_error; name/other fields unaffectedreuses F34 error path
ES5me load failure on Account pageskeleton → toast account.error_load—
ES6Non-admin hits /settings directly (org settings)page still loads (routes not role-gated today); org-only mutations 403 server-sideroute-gating is §12 open question

Internal: log password-change failures at WARN without the password value; standard DRF error serialization otherwise. No retry/backoff (interactive forms).

10. Diagrams & Docs Touched

  • frontend/DIAGRAMS.md — route tree: add /settings/account; relabel /settings as Organization Settings; sidebar group structure note.
  • backend/DIAGRAMS.md — request lifecycle: new self-scoped account endpoints (/auth/me/ PATCH, /auth/me/change-password/).
  • Related specs (unchanged internals, cross-linked): [[F02-organizations]], [[F03-teams]], [[F33-connected-source-signals]], [[F34-user-uploads]], [[F35-email-notifications]], [[F37-ai-language-contract]], [[F58-support-ticketing]], [[F59-referral-affiliate]].
  • No ADR required (no structural model change). The self-scoped (non-tenant) account endpoints are consistent with existing /auth/me/* precedent.

11. Implementation Audit (2026-06-15)

Source-audited and registered for verification. Shipped via PR #186 (drafted as F72, renumbered to F75 — F72 was reassigned to Freemium upgrade prompts). Backend 2611 tests @ 99.65%; frontend tsc clean + Sidebar 14/14 + AccountPage 4/4; E2E Settings split 3/3 (chromium, local seeded backend). Post-merge hotfix: the PR #186 + HQ/affiliate auto-merge left Sidebar.tsx with a broken group structure (build-breaking on main) — corrected (Personal group made unconditional, HQ group wrapped as ...(isStaff ? [...] : []), isStaff added to the useMemo deps).

#ClaimStatusEvidence
1PATCH /api/auth/me/ self-update (name, avatar_url)✅apps/accounts/views.py me_view; MeUpdateSerializer; TestAccountSelfUpdate
2POST /api/auth/me/change-password/ (validates current + Django validators)✅change_password_view; ChangePasswordSerializer; TestChangePassword
3Self-scoped (not tenant); user A cannot mutate user B✅TestAccountSelfScope in test_multitenancy.py
4Backend gate ≥99%✅2611 tests, 99.65% (one pre-existing unrelated CORS failure)
5Sidebar split: Organization (admin/owner) vs Personal (all members)✅Sidebar.tsx; Sidebar.test.tsx 14/14
6New /settings/account AccountPage (name, avatar, password, language, notif prefs)✅pages/AccountPage.tsx; route in App.tsx + routeTree.ts; authApi.updateProfile/changePassword
7SettingsPage relabelled Organization Settings; notif prefs moved to Account✅SettingsPage.tsx diff; account.json
8i18n en + vi for account + sidebar group keys✅locales/{en,vi}/account.json, sidebar.json
9Frontend gate: tsc --noEmit clean✅verified on branch
10AccountPage unit test (mocked authStore + endpoints)✅src/test/AccountPage.test.tsx 4/4 (render, profile save, password mismatch blocks submit, valid change calls API) — single-fork verified
11E2E Settings split describe (3 cases: admin both groups, member Personal-only, member opens /settings/account)⚠️ written, not runfrontend/tests/e2e/lumi.part09.spec.ts describe("Settings split (F75)"), href-based asserts; cannot run in this env (Playwright webServer expects backend/venv/bin/python, absent under uv) and a pre-existing duplicate test-title on main (F49 GET /api/projects/:id/board/… at L2873 & L3140) breaks Playwright collection suite-wide
12Full vitest suite run locally⚠️ envparallel pool OOMs this box; ran single-fork for affected files (Sidebar 14/14, AccountPage 4/4)
13Visual QC: organization settings uses choice controls for canonical org metadata instead of exposing raw machine-ish free-text fields✅2026-06-18 visual QC: SettingsPage.tsx promotes industry + company size to selects, reusing onboarding industry labels and stable size options; screenshot slice shows human-readable org metadata instead of the raw other scaffold.
14Visual QC: settings sections announce context before a dense stack of forms✅2026-06-18 visual QC: /settings now opens with compact org/access summary chips, frames create/join flows as separate sub-panels, and gives the org switcher a short operational hint so the page reads as settings narrative rather than one long admin form.

Next (to flip E2E ✅): in a CI/full env, (a) fix the pre-existing F49 duplicate test-title (out of F75 scope — blocks the whole suite), then (b) run make e2e and confirm Settings split (F75) passes. The E2E cell stays 🔲 until that run is green.

Original design constraints (pre-build)

  • User.avatar_url already exists (apps/accounts/models.py:28) — no migration needed for avatar.
  • me_view (apps/accounts/views.py:174) is GET-only — PATCH self-update and change-password are net-new view functions + URL entries in apps/accounts/urls.py.
  • Avatar upload must reuse F34 presign (/api/uploads/presign/) — do not add a second upload path.
  • Notification preferences already per-user (/notification-preferences/me/) — move the UI block; do not duplicate the endpoint.
  • Sidebar role derivation (myRole/isAdminOrOwner) is the existing gate — reuse it; do not invent a new permission check.
  • Routes in App.tsx are not role-gated today (only hidden from sidebar). Adding /settings/account follows that pattern; route-level gating for org-only pages is deferred (see §12).

12. Open Questions / Decisions

  • Decided (2026-06-15, user): Build a full personal Account page (name, avatar, password, language, personal notifications) — not just a re-bucketing. Org-level settings (name, billing, danger zone, switch/create/join) stay together under the Organization menu as “Organization Settings”.
  • Open: Should org-only pages (/settings, /settings/teams, /settings/invite, /settings/sources) become route-gated (redirect non-admins) rather than only hidden from the sidebar? Currently they are reachable by direct URL and rely on server-side 403 for mutations. Low risk, but worth a follow-up.
  • Open: Force re-login / token rotation after a password change? MVP keeps existing sessions valid; revisit if F39 security audit requires session invalidation.

13. Bug / Change Log

2026-07-02 — Spec drift correction: check all 6 ACs; status “draft” → “shipped (E2E pending)“

2026-07-16 — Contract Graph evidence audit

Type: Evidence correction

The shipped web, self-account, and shared settings sources were reconciled into F75.cue. The browser row remains registered but unexecuted, and native source/unit coverage is not promoted to named-device runtime evidence. Missing local runner environments and the known F49 Playwright collection conflict remain explicit blockers.

Type: Documentation (spec drift fix)

Summary:

  • Corrected §2 AC checkboxes to reflect the actual implementation state per the §11 audit.
  • Checked all 6 previously-unchecked ACs: member Personal-group visibility (§11 row 5 ✅), AccountPage content: name/avatar/password/language/notif prefs (row 6 ✅), admin/owner dual-group sidebar (row 5 ✅), Organization Settings content (row 7 ✅), wrong-password → 400 (row 2 ✅), self-scope isolation (row 3 ✅).
  • Updated status from “draft” to “shipped (E2E pending)”: the §11 audit intro says “Built and verified (backend + frontend + E2E)” and all backend + frontend rows are ✅; only row 11 (E2E written but not yet run in a full-Playwright env) is ⚠️, keeping the E2E matrix cell 🔲 until a green run against the seeded backend.
  • No code, test, API, or behavior change.

2026-06-18 — Visual QC: promote org metadata fields from raw text inputs to explicit choices

Type: UX Fix

Summary:

  • The visual audit of /settings showed the organization form reading like an internal scaffold: industry and company_size were plain text inputs, so the screenshot surfaced raw values such as other instead of user-facing choices.
  • The settings form now uses explicit choice controls for industry and company size, reusing the onboarding industry vocabulary and stable company-size ranges.
  • This makes the organization settings page feel like a finished product surface rather than a backend admin form.

2026-06-18 — Visual QC: add context chips and calmer access panels

Type: UX Fix

Summary:

  • The settings page still felt visually dense after the metadata-field cleanup because every section began at the same visual weight.
  • Added a compact summary row for current org, org count, and access level so the first viewport explains what kind of settings surface this is.
  • The create/join organization forms now sit in their own bordered sub-panels, and the switcher includes a short hint about what switching actually changes.
  • This keeps the long page readable without changing any settings behavior.

Verification:

  • frontend/screenshots/14-settings/01-settings-page.png
  • frontend/screenshots/14-settings/02-settings-scroll.png

Verification:

  • frontend/src/test/SettingsPage.test.tsx
  • frontend/screenshots/14-settings/01-settings-page.png

2026-06-15 — Frontend route namespaced

Frontend route namespaced: /teams -> /settings/teams, /invite -> /settings/invite, /sources -> /settings/sources, /account -> /settings/account (app-wide IA restructure; API paths unchanged).

2026-06-15 — Tests added: AccountPage unit (4/4) + E2E block (written, not run)

Type: Feature

Summary:

  • Added src/test/AccountPage.test.tsx (4 tests, all passing single-fork): renders sections + name, saves profile via authApi.updateProfile, password mismatch blocks submit, valid change calls authApi.changePassword(current, new).
  • Added tests/e2e/lumi.spec.ts describe("Settings split (F75)") (3 href-based cases).

Notes:

  • E2E could not be executed here: Playwright webServer expects backend/venv/bin/python (absent under uv), and a pre-existing duplicate test-title on main (F49 board test, L2873 & L3140) breaks Playwright collection for the whole suite. E2E matrix cell remains 🔲 until a green run.
  • Found (untranslated key): the password-mismatch FormMessage renders the raw key password_mismatch (shadcn FormMessage does not run t()); minor i18n polish follow-up.

2026-06-15 — Backend + frontend shipped (E2E pending)

Type: Feature

Summary:

  • BE: PATCH /api/auth/me/ (name, avatar_url) + POST /api/auth/me/change-password/; MeUpdateSerializer, ChangePasswordSerializer; tests TestAccountSelfUpdate/TestChangePassword/TestAccountSelfScope.
  • WB: Sidebar split into Organization (admin/owner) and Personal (all members) groups; new /account AccountPage; SettingsPage relabelled Organization Settings (notif prefs moved to Account); i18n en+vi account namespace + sidebar group keys; Sidebar.test.tsx updated.

Gates:

  • Backend: 2611 tests @ 99.65% (≥99%). Frontend: tsc --noEmit clean; Sidebar.test.tsx 14/14.

Known gaps (see §11):

  • No E2E Settings split block yet; no AccountPage unit test; full vitest suite OOMs locally (ran single-fork for affected files). MVP_SCOPE E2E cell stays 🔲; status 🚧 until E2E lands.

Built by CMD-v2 fleet (BE-A 2963acd6, WB-A c3cbb249) on feat/f72-settings-split.

2026-06-15 — Spec created

Type: Feature

Summary:

  • New spec F75 separating the single sidebar Admin group into Organization (admin/owner: Teams, Invite Members, Connected Sources, Organization Settings) and Personal (all members: Account, Support, Referral), and introducing a personal Account page (name, avatar, password, language, personal notification preferences) with new self-scoped backend endpoints.

Reason:

  • Non-admin members currently have no access to support, referral, or personal account controls; and organization vs personal controls are conflated under one menu.

Impact:

  • Sidebar IA, a new /account page + account i18n namespace, relabeled SettingsPage, and two new request.user-scoped account endpoints. No model changes (avatar_url already exists).

Tests:

  • To be added per §7 (backend self-update + change-password + self-scope; frontend Sidebar + AccountPage; E2E “Settings split”).

Notes:

  • Account endpoints are deliberately self-scoped (request.user), not tenant-scoped — the one sanctioned exception to Rule 1, consistent with existing /auth/me/* endpoints.