# SETTINGS COMPLETION AUDIT — what actually works

**Written at v9.10.380.** Owner asked for this so the remaining work can be tackled a few
pieces at a time instead of guessing.

---

## The headline

**40 sub-sections across 8 categories. Exactly 4 of them save anything.** Everything else
renders controls that look live, respond to clicks, and then discard the result on close.

That is worse than a "SOON" label, because a toggle that flips and forgets teaches the user the
setting is applied. **24 SOON markers** are honest; the ~30 silently-inert controls are not.

| Category | Subs | Save? | Honest SOON | Silently inert |
|---|---|---|---|---|
| Account | 6 | Profile only | 9 | Sign-in toggles, Danger Zone |
| Subscription & Usage | 6 | none | 4 | Extra-credits toggle |
| Internet & Data | 4 | **honest (v9.10.396)** | 5 | — |
| Ecosystem | 5 | **all wired (v9.10.381/384)** | 2 | — |
| Agents | 5 | none | 5 | Behavior, Memory, Automations |
| Appearance | 5 | **all wired (v9.10.382)** | 0 | — |
| Privacy | 6 | **all wired (v9.10.381/383)** | 0 | — |
| General | 3 | **Startup wired (v9.10.397)** | 1 (Language) | — |

---

## Priority order (recommended)

### P0 — the ones that are actively misleading
Privacy and Ecosystem/Notifications are the worst offenders: a user toggles "Hide my presence"
or "Don't notify me about X", the switch moves, and nothing changes. A privacy control that lies
is a genuine trust problem, not a polish item.

1. ~~**Privacy → Visibility / Presence / Read receipts**~~ — **DONE (v9.10.381).** Persisted AND
   enforced at 8 server call sites.
2. ~~**Ecosystem → Notifications**~~ — **DONE (v9.10.381).** 8 per-kind switches, filtered server-side.
3. ~~**Privacy → Blocked**~~ — **DONE (v9.10.383).** Full block list end-to-end: store, endpoints,
   and enforcement in both directions across DMs, feed, discovery, presence, profiles, friend
   requests and notifications.

### P1 — cheap wins, already have a backend
4. ~~**Appearance → Density & Text / Motion**~~ — **DONE (v9.10.382).** All six Appearance keys save
   and apply live; Reduce motion genuinely suppresses the wallpaper parallax.
5. ~~**Account → Sessions & Devices**~~ — **already complete** (audit was wrong; revoke works on both
   sides and refuses to kill the current session).
6. ~~**Internet & Data → Web Browser / Connections**~~ — **DONE (v9.10.396).** Search-engine dropdown
   was fiction (provider is key-based) → now reports the real one; export wired from Data & Storage;
   a third autoplay control folded onto the shared key; four given honest SOONs with reasons.

### DONE since this audit
- Ecosystem → Feed / Channels / Content Filters (muted words enforced server-side) — v9.10.384
- Danger Zone → Export my data — v9.10.384

- General → Startup (real view ids + honoured on load) and About (real version) — v9.10.397

### P2 — needs new backend
7. **Agents → Behavior / Memory / Automations** — real feature work, not settings plumbing.
8. **Subscription → Billing / Invoices** — shadow billing exists; real billing does not.
9. **Account → Sign-in & Security** — password change, 2FA, login alerts.
10. ~~**Account → Danger Zone**~~ — **DONE.** Export v9.10.384; Deactivate + Delete v9.10.398 with the
    owner-approved cascade (comments kept but de-identified, DM threads kept for the other party,
    handles retired permanently, 30-day grace, scheduled purge).

---

## Missing sections the owner asked for

**Activity log / timeline** — **DONE (v9.10.382)**, Account → Activity, derived at read time.
Original finding kept below for context. It did not exist anywhere in Settings. It should live under
**Account → Activity**, and there is already plenty to populate it from — every write path is
recorded somewhere:

- posts/comments/likes (`social_feed`), shorts, DMs
- friend requests + accepts
- logins and sessions (`sessions` map has `createdAt`, `ua`, `ip`)
- profile edits, plan changes, credit spend (`credits.mjs` has a ledger)
- badge awards (`badges.json` stores an ISO date per badge)

Suggested shape: one merged, reverse-chronological feed with a type filter, reading from the
existing stores at request time (same derive-at-read-time pattern as notifications — no new
write path, nothing to keep in sync). Add `GET /api/me/activity?limit=&type=`.

**Also missing:** a Notifications *history* view (the bell only shows current items), and an
account-wide search for your own content.

---

## The pattern to fix, not just the instances

Every inert control shares one cause: `_s2tog()` renders a switch whose only behaviour is
`s2Toggle(this)` flipping its own colour. There is no id, no binding to a settings key, and no
save path.

**Fix the component, not 30 call sites:** give `_s2tog(on, key)` a settings key, have it write
into a pending-changes object, and add a single save per pane. Then each remaining task is
"wire key X to behaviour Y" rather than "build a settings form". Roughly:

```
_s2tog(cur.notifyMentions, 'notifyMentions')   → pending.notifyMentions = true/false
                                                → PATCH /api/me/settings on Save
```

`/api/me/settings` already exists with `sanitizeUserSettings()` + `mergeUserSettings()`, so the
storage half is done. The gap is purely that the UI never calls it.

---

## Rules for whoever picks this up

1. **A control that cannot be honoured yet gets a SOON tag, not a working-looking switch.**
   Inert-but-convincing is the one outcome to avoid.
2. **Persist AND enforce.** Saving `hidePresence: true` is half a feature; the presence endpoint
   has to read it.
3. **Privacy defaults stay private.** When adding a toggle, the safe value is the default.
4. Add each newly-wired key to the settings test surface so it cannot silently regress.

---

## Two settings systems (found v9.10.387 by `test-settings.mjs`)

There are **two** switch components in `oracle.html`, rendering identically:

| Component | Persists to | Scope |
|---|---|---|
| `_s2tog()` / `_s2Stage()` | `PATCH /api/me/settings` | **account** — follows you everywhere, in the export, enforceable server-side |
| `gmSw()` with `data-key="oracle.*"` | `localStorage` only | **device** — this browser only |

The 9 `oracle.*` switches do persist, so they are not the flip-and-forget problem. But being
device-level means they do not follow you to another browser or your phone, they are absent from
the data export, and nothing server-side can act on them.

**Three of them shadow an account-level key**, so the two can disagree about the same preference
with no way for the user to tell which is winning:

| localStorage key | account key |
|---|---|
| `oracle.reduceMotion` | `reduceMotion` |
| `oracle.autoplay` | `feed.autoplayShorts` |
| `oracle.compactFeed` | `density` |

**RESOLVED (v9.10.388).** The three shadowing switches now WRITE THROUGH to their account key via
`GM_ACCOUNT_MAP`, and read from the account rather than localStorage — so the account is the single
source of truth and the two can no longer disagree. Fixed in `gmSw()` / `gmInitSwitches()` (the
component) rather than in the three legacy rows, so any future row using those keys inherits it.
localStorage is still mirrored, so a failed/offline save degrades to the old behaviour instead of
losing the toggle. `density` needed special handling: it is an enum, not a flag, so "Compact feed"
ON maps to `'compact'` and OFF to `'normal'`.

`test-settings.mjs` now ASSERTS the write-through (it would previously only have reported the
clash), so silently reverting to localStorage-only fails the suite.

**Still device-local by design** (no account equivalent, and arguably correct as per-device):
`oracle.aiCanDm`, `oracle.aiInFeed`, `oracle.dataSaver`, `oracle.deepThink`, `oracle.safeBrowsing`,
`oracle.showTokens`. The suite prints this list every run so it cannot quietly grow.

