Files
OwnCord/docs/architecture/ux/settings-and-admin.md
T

220 lines
13 KiB
Markdown
Raw Normal View History

# Settings & Admin — target UX
**Verified against:** commit `5630aa1`, 2026-08-04
Part of the [Client UX Specification](README.md).
Covers: the settings overlay and its tabs, account operations (profile, password,
2FA, delete), appearance/theming, the client's **inline** admin surface (ban/kick/
roles, channel CRUD, invites), and the updater. It also marks the boundary
between what the desktop client does and what lives only on the server web panel.
---
## 1. Settings overlay
A tabbed overlay (`SettingsOverlay`) available both authenticated (in Main) and
unauthenticated (on Connect, for appearance/advanced). Tabs: Account,
Appearance, Notifications, Text & Images, Accessibility, Voice & Audio, Keybinds,
Advanced, Logs.
**Target rules:**
- Every save is confirmed: a toast on success, an inline error on failure. No
silent saves.
- Preference writes are immediate and local (localStorage `owncord:settings:*`),
broadcast via the `owncord:pref-change` event so open views re-read live (e.g.
theme, message density) without a restart.
- Structural/durable data (server profiles, window geometry, per-user volumes)
persists through the Rust key-allowlisted store (`settings.json`); lightweight
UI prefs through localStorage. This split is intentional; the spec preserves it.
---
## 2. Account operations
The Account tab holds the identity-sensitive flows. All require the current
password for sensitive changes and are rate-limited server-side.
### 2.1 Profile edit
| Step | Reaction |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Edit username/avatar | `PATCH /users/me`; optimistic `auth.updateUser`; server broadcasts `user_update` so the member list + own bar update live |
| Failure | Inline field error + rollback |
### 2.2 Change password (with session revocation)
```mermaid
sequenceDiagram
autonumber
participant U as User
participant A as AccountTab
participant API as api.ts
U->>A: current + new + confirm (new min 8, new equals confirm)
A->>API: PUT /users/me/password
alt success
API-->>A: 204 — all other sessions revoked, this one kept
A->>U: "Password changed" toast, fields cleared
else revoke step failed
API-->>A: 200 warning, sessions_revoked
A->>U: success plus note — some sessions may still be active
else wrong current password
API-->>A: 403 — lockout counter server-side
A->>U: inline "Incorrect password"
else weak or same-as-old
API-->>A: 400
A->>U: inline validation error
end
```
**Target rule (the W2-2 contract, surfaced to the user):** once the password is
committed, the operation is a **success** even if the session-revocation step
fails — the UI must never present a committed change as an error (that would walk
the user into the confirm-lockout). The partial-success `200 {warning}` maps to a
success message with a soft note, never a red error. (Server contract:
`handleUpdateProfile()` in `Server/api/profile_handler.go`; client already toasts success, the `onUpdateProfile` handler in `pages/MainPage.ts`.)
### 2.3 Two-factor (TOTP)
| Flow | Steps |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Enable | Password prompt → `POST /totp/enable` → render QR URI + backup codes → 6-digit confirm → `POST /totp/confirm` → "Enabled" badge, `auth` user `totp_enabled:true` |
| Disable | Password confirm → `DELETE /totp`; a `403`/"required" is rewritten to "2FA is required by this server and cannot be disabled" (already the 403 rewrite in `buildTotpDisableView()`, `components/settings/AccountTab.ts`) |
**Target rule:** backup codes are shown exactly once, with an explicit "Save these
now — you won't see them again" and a copy affordance.
### 2.4 Sessions & delete account
| Action | Reaction |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| List sessions | `GET /users/me/sessions`; show device/IP/last-used; current session marked |
| Revoke a session | `DELETE /users/me/sessions/{id}`; optimistic removal + toast |
| Delete account | **Modal with password confirm** (irreversible — stronger than a two-click); `DELETE /auth/account``clearAuth()` → connect page |
---
## 3. Inline admin surface (client)
The desktop client exposes a **subset** of admin operations inline, gated by the
actor's role. Everything here must (a) only appear for users who can perform it,
and (b) confirm destructive actions.
| Operation | Affordance | REST | Reaction |
| ---------------- | ------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------- |
| Change role | Member context menu → submenu | `PATCH /admin/api/users/{id}` `{role_id}` | Toast; `member_update` reflects live |
| Kick | Member menu, two-click confirm | `DELETE /admin/api/users/{id}/sessions` | Toast "Kicked {user}"; `member_leave` |
| Ban | Member menu, two-click confirm | `PATCH /admin/api/users/{id}` `{banned, ban_reason}` | Toast; `member_ban` removes them |
| Create channel | Sidebar → modal | `POST /admin/api/channels` | Modal closes on success; `channel_create` |
| Edit channel | Channel menu → modal | `PATCH /admin/api/channels/{id}` | `channel_update` |
| Delete channel | Channel menu, two-click confirm | `DELETE /admin/api/channels/{id}` | `channel_delete`; redirect if active |
| Reorder channels | Drag | `PATCH …/{id}` `{position}` per moved | Optimistic; roll back on failure |
| Invites | Invite manager modal | `GET/POST/DELETE /invites` | List with masked codes, copy, revoke; empty state "No active invites" |
**Target rules:**
- **✓ Destructive admin actions show an in-flight state (2026-08).**
`withConfirmation` (`AdminActions.ts`) keeps the item in a pending
label/class while the promise settles and ignores further clicks, so a slow
ban no longer looks ignored; unblock, ban submit, purge, and the role-change
submenu carry their own equivalent guards (a role change in flight also
inerts the other role options — `currentRole` only updates when the
`member_update` echoes).
- **✓ Ban collects a reason (2026-07/08).** The ban flow renders an inline
reason input plus a duration choice (`appendBanFlow()` in `components/AdminActions.ts`),
and the menu passes both through
(the `onBan` handler in `createSidebarMemberSection()`, `pages/main-page/SidebarMemberSection.ts``api.adminBanMember(userId, reason,
durationHours)`), so temporary bans and stored reasons work from the client.
### 3.1 What is _not_ in the client (by design)
The full admin panel — user list, audit log, server settings, channel
permissions, plugin management, backups, updates, first-run setup — is the
**server-rendered web panel** under `/admin`, gated by IP restriction + admin
auth. The Tauri client has **no** REST methods for these (confirmed: no plugin/
audit/settings/permissions/setup calls in `api.ts`). The one bridge the client
does have is a deep-link: `lib/admin-panel.ts` opens
`https://{host}/admin#{section}` in the OS browser (wired from
the Audit Log button handler in `createSidebarArea()`, `pages/main-page/SidebarArea.ts`, gated by
`lib/permissions.ts::canViewAuditLog`).
> **Decision point.** If the target is for admins to manage the server from the
> desktop app (audit log, settings, plugins) rather than the web panel, that is a
> **new surface** to build, not a gap in an existing flow. Flagged here so the
> boundary is explicit; the current split (inline moderation in the client, full
> administration on the web) may well be the intended design.
---
## 4. Appearance & theming
Themes are CSS custom properties (`styles/tokens.css`), 4 built-ins
(`dark | neon-glow | midnight | light`) plus custom overrides. **Target:** theme
changes apply **live**`ui.setTheme` + the `owncord:pref-change` event re-skin
open views without reload. Appearance is editable pre-login (on the connect page)
so the app respects the user's theme before they authenticate.
---
## 5. Updater
Self-hosted: the update endpoint derives from the connected server URL, over
TOFU-pinned (or system) TLS, minisign-verified inside the Tauri updater plugin.
```mermaid
sequenceDiagram
autonumber
participant N as UpdateNotifier
participant R as Rust updater
N->>N: 3s after mount → check_client_update(server_url)
alt update available
R-->>N: {available, version, body}
N->>U: banner "Update vX available" [Update Now] [Later]
U->>N: Update Now
N->>N: banner → "Downloading update…"
N->>R: download_and_install_update (minisign verify + apply)
R-->>N: (success → relaunch())
else up to date / check failed
N->>N: no banner (or "Update failed. Try again." + Dismiss)
end
```
| State | Presentation |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| checking | Silent (no UI until a result) |
| available | Non-modal banner with version + Update Now / Later (already `createUpdateNotifier()`/`showBanner()`, `components/UpdateNotifier.ts`) |
| downloading | Banner "Downloading update… N%" (or "… N.N MB" until Content-Length is known) |
| applied | App relaunches automatically |
| failed | "Update failed. Please try again later." + Dismiss |
> **✅ Wired — download progress.** The Rust download callback
> (`download_and_install_update` in `update_commands.rs`) accumulates received
> bytes and emits an `update-progress` event (`{ received, total }`) to the
> webview. `downloadAndInstallUpdate(serverUrl, onProgress)` (`updater.ts`) listens
> for it and forwards to `UpdateNotifier`, whose `formatDownloadProgress` renders a
> percentage when `total` is known and falls back to bytes (MB) otherwise, so the
> banner never looks hung. (Rust change is minimal and CI-gated only.)
---
## 6. System tray
The tray icon (`src-tauri/src/tray.rs`) is a parallel presence/window surface:
**Show/Hide** toggles the main window, a **Status** submenu
(Online / Idle / Do Not Disturb / Offline) emits a `status-change` event that
the TS side applies through the same presence path as the user-bar picker
(`lib/userStatus.ts` / `components/StatusPicker.ts`), and **Quit** exits the
app.
---
## Source of truth
`src/components/SettingsOverlay.ts` (+ `settings/*`), `src/components/AdminActions.ts`,
`src/components/InviteManager.ts`, `CreateChannelModal.ts`, `EditChannelModal.ts`,
`DeleteChannelModal.ts`, `src/components/UpdateNotifier.ts`, `src/lib/updater.ts`,
`src/lib/api.ts`, `src/lib/themes.ts`, `src/lib/preferences.ts`,
`src/pages/main-page/SidebarArea.ts`, `SidebarMemberSection.ts`,
`OverlayManagers.ts`, `src-tauri/src/commands.rs`, `src-tauri/src/update_commands.rs`;
server `Server/admin/api.go`, `Server/api/profile_handler.go`, `totp_handler.go`.