# Settings — per-pillar + cross-pillar

**Job:** One Settings surface, multiple **owners**. Each pillar owns its prefs;
the shell owns presentation and cross-pillar chrome.

One-liner: *Who configures what — without one giant `settings.json` mental model.*

---

## Model

```
Settings (macOS SetupView / iOS SettingsView)
├── Cross-pillar     — theme, build info, fleet gate (identity shell)
├── Watch            — playback, Now Playing, Home rails, skip intro
├── Library          — types, folders, scan, grouping, metadata defaults
├── Download         — notify transfers, bandwidth, search defaults
├── Net              — subscriptions, contribute, trust per part
└── Devices          — registry, mesh join, storage pools, offline policy
```

**Not three apps** — one container, **sectioned** settings. Sections map 1:1 to
pillar owners even when stored in one file today.

---

## Storage layout (target)

Split tolerant decode from one blob → namespaced keys:

```jsonc
{
  "cross": {
    "appTheme": "winamp-classic",
    "winampSkinId": null,
    "winampSkinName": null
  },
  "watch": {
    "playbackMode": "stream",
    "adultPlaybackMode": "stream",
    "forwardMediaKeys": true,
    "skipIntroSeconds": 90,
    "showContinueWatchingOnHome": true,
    "showRecentlyAddedOnHome": true,
    "hoverPreviews": false,
    "pornFeature": false,
    "surfaceAdultOnHome": false,
    "switchToAdultOnlyHome": false,
    "adultQueueCount": 25,
    "playbackDisplayId": null
  },
  "library": {
    "combineSplitShows": true,
    "useLLMGrouper": false,
    "folderTypes": {},
    "libraryTypes": [],
    "episodeDisplayStyle": "codeAndTitle",
    "preferEnrichedTitles": true
  },
  "download": {
    "notifyDownloads": true
  },
  "net": {
    "subscriptions": [],
    "contributeObservations": true
  },
  "devices": {
    "vpnProfiles": [],
    "localOfflinePolicy": {}
  }
}
```

**Migration:** v1 flat `AppSettings` decodes into nested sections; encode can
stay flat until a cutover release. `AppSettingsPatch` gains optional section
keys.

---

## Section → pillar map (v1 fields today)

| Section | Fields (current `AppSettings`) |
|---------|--------------------------------|
| **cross** | `appTheme`, `winampSkinId`, `winampSkinName` |
| **watch** | `playbackMode`, `adultPlaybackMode`, `forwardMediaKeys`, `skipIntroSeconds`, Home rails, adult toggles, `playbackDisplayId`, `hoverPreviews` |
| **library** | `combineSplitShows`, `useLLMGrouper`, `folderTypes`, `libraryTypes`, `episodeDisplayStyle`, `preferEnrichedTitles` |
| **download** | `notifyDownloads` (+ future `bandwidthPolicy`) |
| **net** | *(planned)* subscriptions, contribute |
| **devices** | VPN lives in Keychain + `VPNController`; per-device `offlinePolicy` in `devices.json` |

Device-specific prefs stay in `~/.config/tv-anarchy/devices.json` under the
**Devices** pillar — not in `settings.json`.

---

## UI (macOS `SetupView` target)

| Section | Pillar | v1 location |
|---------|--------|-------------|
| Appearance | cross | Appearance (theme, Winamp) |
| Home | watch | Home |
| Playback | watch | Playback |
| Library types / folder types | library | Library types sections |
| Downloads notify | download | Playback (today — move) |
| Net | net | *new* |
| Devices / storage pools | devices | Mesh join, VPN; extend Devices tab |
| VPN | devices | VPNSettingsView |

Phase 6: sidebar **Settings** opens with pillar section headers, or sub-nav
list (Watch · Library · Download · Net · Devices · General).

---

## API

`AppLocalAPI` patches address a section:

```http
PATCH /settings/watch { "skipIntroSeconds": 120 }
PATCH /settings/cross { "appTheme": "standard" }
```

MCP tools mirror section names — avoids ambiguous flat patches.

---

## Tests

| Test | Asserts |
|------|---------|
| `SettingsStoreTests` | Flat ↔ nested round-trip |
| `AppLocalAPITests` | Section-scoped patch |
| Tolerant decode | Missing section → defaults |

See [plan.md § Settings split + full best-practices Appendix C](../plan.md) (includes how pillar sections + cross theme achieve ownership while DRY, and theming details).

**Implementation note (2026-06 build-out)**: Nested is decode-only for now (tolerant probe for `cross`/`watch`/etc containers or legacy flat root). Encode stays flat until explicit cutover. `AppLocalAPI` + MCP will route `/settings/<section>`. Moves (e.g. notifyDownloads) + new Net/Devices sections happen in SetupView reorg. Update correlation/state.md + ui.md + components.md on changes.