---
module: profile.appearance
title: Appearance
tab: Claude
order: 40
audience: dev
---

Terse agent reference. **Owner: hiveops-auth.** Self-service per-user preference. No Kafka, no admin gate.

## Ownership
- Data + API owner: **hiveops-auth** (port 8082, nginx `/api/auth/`).
- UI: `hiveops-profile` SPA (`profile.bcos.cloud`, dev 5184), `AppearanceSettings.svelte`, view id `appearance`.
- Only two keys: `themeMode`, `tableDensity` — subset of the shared preferences JSON.

## Endpoints (confirmed in code)
| Method | Path | Body | Returns |
|--------|------|------|---------|
| GET | `/auth/api/users/me/preferences` | — | `UserPreferences` |
| PUT | `/auth/api/users/me/preferences` | `UpdatePreferencesRequest` (partial, null-skip) | updated `UserPreferences` |

- Controller: `UserProfileController` (`@RequestMapping "/auth/api/users/me"`).
- Frontend axios baseURL = `window.__APP_CONFIG__.apiUrl` (gateway); Bearer JWT from `localStorage.token`.

## Auth / roles
- `SecurityConfig`: `.anyRequest().authenticated()` — **any authenticated user**, NO `hasAnyRole(...)`.
- Target row = caller's own, resolved via `@AuthenticationPrincipal` → `findByEmail`.

## Data store (confirmed)
| Item | Value |
|------|-------|
| DB | `hiveiq_auth` (Postgres, VM `10.10.10.188` via ProxyJump through `173.231.195.250`) |
| Table | `users` |
| Column | `preferences` (JSONB, `NOT NULL DEFAULT '{}'`), migration `V9__add_user_preferences.sql` |
| Mapping | `@JdbcTypeCode(SqlTypes.JSON)` → `UserPreferences` POJO |

Appearance keys inside JSONB:
- `themeMode`: `LIGHT` | `DARK` | `SYSTEM` (default `SYSTEM`)
- `tableDensity`: `comfortable` | `compact` (default `comfortable`)

```sql
SELECT preferences->>'themeMode', preferences->>'tableDensity'
FROM users WHERE email='...';
UPDATE users SET preferences = preferences
  || '{"themeMode":"SYSTEM","tableDensity":"comfortable"}' WHERE email='...';
```

## Kafka / topics
- **None.** No producer/consumer for this feature. Do not look for one.

## Gotchas
- Theme is applied client-side (`dark-mode` class on `<html>`); `SYSTEM` = `matchMedia(prefers-color-scheme)`, not server-driven.
- Electron cross-module sync via `window.electronAPI.setTheme(...)`; plain web tabs update on next prefs load.
- PUT is a partial merge — sending only `{themeMode,tableDensity}` will NOT wipe notification/security prefs.
- `themeMode`/`tableDensity` have NO server-side validation (plain `String`, no `@Pattern`/enum) → arbitrary strings persist.
- Save error handling is a generic toast; a 401 (expired JWT) shows as "Save failed".

## Do NOT
- Do NOT look for an appearance controller/table in `hiveops-profile` — it is frontend-only; backend is `hiveops-auth`.
- Do NOT assume an admin role or fleet/institution-wide theme setting exists — this is per-user only.
- Do NOT expect a Kafka event, executor, or record-store on preference change.
- Do NOT `exec` Postgres on the services VM (`173.231.195.250`) — `hiveiq_auth` is on `10.10.10.188`.
- Do NOT invent a `theme` / `preferences` REST path other than `/auth/api/users/me/preferences`.
