---
module: profile.appearance
title: Appearance
tab: Internal
order: 20
audience: internal
---

Ops/support view of the **Appearance** panel (theme + table density) in the Profile SPA. This is a **per-user, self-service** preference — there is nothing admin-only here and no fleet/institution-wide toggle.

## What it actually is
- Frontend: `hiveops-profile` SPA (`profile.bcos.cloud`, dev port **5184**), component `AppearanceSettings.svelte`, view id `appearance`.
- Backend: **hiveops-auth** (port 8082, nginx `/api/auth/`). Appearance is just two keys inside the user's preferences JSON.
- Two settings only: `themeMode` (`LIGHT` | `DARK` | `SYSTEM`, default `SYSTEM`) and `tableDensity` (`comfortable` | `compact`, default `comfortable`).

## Roles required
- **Any authenticated user.** The endpoints fall under `.anyRequest().authenticated()` in `SecurityConfig` — there is **no** `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` gate. CUSTOMER, ADMIN, MSP_ADMIN, BCOS_ADMIN can all read/write **their own** prefs.
- User is resolved from the JWT via `@AuthenticationPrincipal` → `findByEmail`. A user can only ever change their own row; there is no "edit another user's appearance" path.

## First things to check when it misbehaves
1. **"Save failed" toast** — the save calls `PUT /auth/api/users/me/preferences`. Check:
   - Token present/valid? Expired JWT → 401 → generic "Save failed". Have the user re-login.
   - hiveops-auth up? `curl http://localhost:8082/actuator/health` on the services VM.
   - CORS: `profile.bcos.cloud` must be in `CORS_ALLOWED_ORIGINS` for hiveops-auth (it is, per auth CLAUDE.md). A new origin that 500s the whole service on boot will take this down too.
2. **Theme resets after reload / doesn't persist** — the value did not reach the DB. Confirm the JSON in Postgres (see below). If `preferences` is `{}` the PUT never landed.
3. **Dark mode "sticks" in one SPA but not others** — theme is applied **client-side** by toggling the `dark-mode` class on `<html>`; each SPA applies it on its own load from the same stored `themeMode`. A stale tab won't repaint until it re-reads preferences. In the Electron Browser, save also calls `window.electronAPI.setTheme(...)` so other modules follow immediately; in a plain web tab they do not.
4. **`SYSTEM` looks wrong** — `SYSTEM` follows `window.matchMedia('(prefers-color-scheme: dark)')`, i.e. the OS/browser setting, not anything server-side. If the OS is light, `SYSTEM` renders light. Working as designed.

## Inspect / fix in the DB
Preferences live as a JSONB blob on the user row in `hiveiq_auth`:

```sql
-- read
SELECT email, preferences->>'themeMode' AS theme,
       preferences->>'tableDensity' AS density
FROM users WHERE email = 'user@example.com';

-- force-reset appearance to defaults (leaves other prefs intact)
UPDATE users
SET preferences = preferences || '{"themeMode":"SYSTEM","tableDensity":"comfortable"}'
WHERE email = 'user@example.com';
```

(Postgres is on the **hiveiq-database** VM `10.10.10.188`, reach it via ProxyJump through `173.231.195.250` — not the services VM.)

## Common failure modes + fixes
| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| Save fails for everyone | hiveops-auth down / CORS boot failure | Health-check auth; verify `CORS_ALLOWED_ORIGINS` includes profile origin; restart `hiveiq-auth` |
| Save fails for one user | expired/invalid token | Re-login |
| Theme not persisting | PUT never reached (network/401) | Check browser devtools; confirm `preferences` JSONB updated |
| Garbage theme value stored | no server-side validation on `themeMode`/`tableDensity` (see Architect) | Reset the JSON keys via SQL above |
| Density change has no visible effect | consuming SPA may not honor `tableDensity` yet (unverified per-app) | Not an auth bug; treat as frontend feature request |

## Escalate to engineering when
- Preferences write succeeds (200) but JSONB doesn't change → possible Hibernate JSON mapping / transaction issue in `UserProfileService.updatePreferences`.
- Invalid `themeMode`/`tableDensity` strings are being persisted from a client → backend has no enum/`@Pattern` guard on these two fields (see Architect notes).
