# TUI Service Menu — Post-Identity Service Selection

> **Status:** Design. This document extends [`plans/tui_login.md`](tui_login.md) with a second screen inside the same `nostr-id-tui` binary: after the user has successfully loaded an identity (mnemonic / generated / imported / bunker / amnesia), the TUI presents a menu of **optional services** and lets the user toggle each one on or off **before** `getty@tty1` is allowed to start.
>
> Depends on: [`plans/tui_login.md`](tui_login.md), [`plans/identity_subsystem.md`](identity_subsystem.md), [`plans/iso_architecture.md`](iso_architecture.md), [`plans/nostr_config_projection.md`](nostr_config_projection.md), [`/home/user/lt/aesthetics/TUI.md`](/home/user/lt/aesthetics/TUI.md).

## 0. Policy

1. **n-OS-tr always installs and sets up every service.** Binaries, unit files, config templates, data directories — all present on the image.
2. **No service starts automatically at boot except the identity/boot pipeline itself.** Exactly these three are enabled image-wide: [`n-os-tr-firstboot.service`](../iso/config/includes.chroot/etc/systemd/system/n-os-tr-firstboot.service), `nostr-id.service` (identity-agent), [`nostr-id-tui.service`](../iso/config/includes.chroot/etc/systemd/system/nostr-id-tui.service). Everything else ships `disabled` via the live-build preset policy.
3. **The user decides what turns on.** After the user authenticates via `nostr-id-tui`, the TUI presents a service-toggle screen. The user picks what they want running this session.
4. **Selections are session-local in v1.** No persistence across reboots. Every boot starts with the same defaults. A future revision may project selections to a Nostr config slot (§10).
5. **One binary, one ceremony.** The service menu is a new screen inside `nostr-id-tui`, not a separate unit. Rationale discussed in the architect session: the user is already in one authenticated terminal flow; we do not want two back-to-back ceremonies before login.
6. **Default-off by default.** Every toggleable service starts the session in the "off" state. The user must explicitly enable each one they want running. This matches the privacy-forward posture of the project.
7. **Style:** strict adherence to [`/home/user/lt/aesthetics/TUI.md`](/home/user/lt/aesthetics/TUI.md) — single-key actions, terminal-width headers, ANSI underline for hotkeys, canonical menu template (§12 of that doc).

## 1. Scope

### 1.1 Services in the menu (v1)

| Key | Label           | systemd unit              | Default | What it does                                           |
|-----|-----------------|---------------------------|---------|--------------------------------------------------------|
| `r` | `c-relay`       | `c-relay.service`         | off     | Local Nostr relay (NIP-01/42). Admin key = index 2.    |
| `g` | `ginxsom`       | `ginxsom.service`         | off     | Blossom blob server behind nginx. Server key = index 3.|
| `f` | `fips`          | `fips.service`            | off     | Mesh-routing daemon; npub-IPv6 overlay. Key = index 4. |
| `n` | `nginx`         | `nginx.service`           | off     | TLS front door / reverse proxy. Needed by c-relay ws, ginxsom uploads. |
| `l` | `config-loader` | `nostr-config-loader.service` | off | Fetch + materialize user config slots (§ [`nostr_config_projection.md`](nostr_config_projection.md#3-boot-flow)). |
| `w` | `config-writer` | `nostr-config-writer.service` | off | Save tracked home paths back to Nostr on shutdown.    |
| `s` | `ssh`           | `ssh.service`             | off     | OpenSSH server. Keys regenerated at firstboot.         |
| `t` | `tor`           | `tor.service`             | off     | Tor daemon. Default posture = SOCKS-only (client), not exit/relay. |

### 1.2 Services explicitly NOT in the menu

- `nostr-id.service` — identity-agent is always required; it's the thing the TUI talks to.
- `n-os-tr-firstboot.service` — oneshot, runs before the TUI, not user-controlled.
- `nostr-id-tui.service` — the TUI itself.
- `systemd-networkd`/`NetworkManager`/`dbus` — core system plumbing; managed by distro defaults.

### 1.3 Service dependencies the TUI must respect

Some services only make sense together. The TUI enforces these dependencies at toggle time (not at the systemd level — systemd's own `Requires=`/`Wants=` handles the actual startup ordering):

| Service       | Implicit deps (auto-enabled when this is turned on)         |
|---------------|-------------------------------------------------------------|
| `c-relay`     | `nginx` (wss termination and cert)                          |
| `ginxsom`     | `nginx` (serves blobs, proxies uploads)                     |
| `config-writer` | `config-loader` (must have run at boot to know the slots) |

If the user toggles `c-relay` on and `nginx` is off, the TUI:
- Shows a one-line prompt: `c-relay needs nginx. Turn nginx on too? [Y/n]`.
- Default `y` on Enter. If `n`, the toggle is cancelled (we do not start a service that won't be reachable).

Reverse direction (turning `nginx` off while `c-relay` is on): prompt `c-relay will become unreachable without nginx. Stop c-relay too? [Y/n]`.

## 2. Placement in the existing TUI flow

### 2.1 Updated state diagram

```mermaid
stateDiagram-v2
    [*] --> Menu

    Menu --> Enter: 1 / E
    Menu --> Generate: 2 / G
    Menu --> Import: 3 / I
    Menu --> Bunker: 4 / B
    Menu --> Amnesia: 5 / A

    Enter --> SendAgent: BIP-39 ok
    Generate --> SendAgent: confirm words ok
    Import --> SendAgent: file read + wiped
    Bunker --> SendAgent: NIP-46 connected
    Amnesia --> SendAgent: ephemeral key

    SendAgent --> Services: agent returns ok
    SendAgent --> FatalError: agent returns error

    Services --> Services: user toggles
    Services --> Confirm: user presses C (commit)
    Services --> Services: user presses X (skip, all off)

    Confirm --> Apply: user presses Y
    Confirm --> Services: user presses N

    Apply --> Apply: start/stop each selected unit
    Apply --> Exit: all units reached terminal state
    Apply --> ApplyError: any unit failed

    ApplyError --> Services: user reviews, retries

    FatalError --> [*]
    Exit --> [*]
```

The only new states are `Services`, `Confirm`, `Apply`, and `ApplyError`. Everything before `SendAgent` is unchanged from [`plans/tui_login.md §3.1`](tui_login.md#31-state-diagram).

### 2.2 Updated boot sequence

```mermaid
sequenceDiagram
    participant SD as systemd
    participant IA as nostr-id.service
    participant TUI as nostr-id-tui.service
    participant Svc as toggleable services
    participant GT as getty@tty1.service

    SD->>IA: start
    IA-->>SD: listening on control.sock
    SD->>TUI: start (owns tty1)
    Note over TUI: identity screens
    TUI->>IA: load_mnemonic/bunker/amnesia
    IA-->>TUI: ok, main_npub
    Note over TUI: service menu
    TUI->>TUI: user toggles, presses Commit
    TUI->>SD: StartUnit/StopUnit via dbus
    SD->>Svc: start selected services
    Svc-->>SD: active
    SD-->>TUI: job results
    TUI->>SD: exit 0
    SD->>GT: start (ordering allows)
    GT->>User: n-os-tr login:
```

## 3. Screen design

Strict compliance with [`/home/user/lt/aesthetics/TUI.md`](/home/user/lt/aesthetics/TUI.md). Every screen follows the canonical template in §12 of that doc.

### 3.1 Main services screen

```text
===================== SERVICES =====================
                (expands to terminal width)

Identity: npub1abcd…wxyz   (main, index 0)
Session:  live (in-memory, no persistence)

  Key  Service         State       Default  Depends on
  ---  -------------   ---------   -------  -----------
   R   c-relay         [ off ]     off      nginx
   G   ginxsom         [ off ]     off      nginx
   F   fips            [ off ]     off      —
   N   nginx           [ off ]     off      —
   L   config-loader   [ off ]     off      —
   W   config-writer   [ off ]     off      config-loader
   S   ssh             [ off ]     off      —
   T   tor             [ off ]     off      —

Actions:
 R/G/F/N/L/W/S/T) Toggle service
 A) Turn All on
 Z) Turn all off (reset)
 C) Commit and continue
 X) Skip (all off, continue)

Select action: >
```

Formatting rules (all from [TUI.md](/home/user/lt/aesthetics/TUI.md)):

- Header expands to full terminal width using the `=== SERVICES ===` pattern from [`ui.c:print_centered_header`](/home/user/lt/aesthetics/TUI.md:88).
- Column widths are fixed; when terminal is narrower than 60 cols, drop the "Default" and "Depends on" columns (the state is what matters).
- State column is either `[ off ]` or `[ on  ]`. A third transient state `[ ... ]` appears during the Apply phase.
- The first letter of each service name in the Key column is rendered with ANSI underline via `\033[4m<letter>\033[0m`, matching [TUI.md §7](/home/user/lt/aesthetics/TUI.md).
- Prompt at bottom is `Select action: >` per the canonical template.

### 3.2 Confirm screen

Before actually doing anything, we show what will happen:

```text
===================== CONFIRM =====================

The following changes will be applied:

  START   nginx.service
  START   c-relay.service
  START   fips.service

  (5 services remain off)

Proceed? [Y/n] >
```

Only services whose session state differs from their default appear. Pure-default sessions (no changes) go straight from the services screen's `C`/`X` directly to `Apply` with an empty plan and exit.

### 3.3 Apply screen

Live progress. We iterate the plan serially (not concurrently — systemd handles that anyway, and serial output is easier to read):

```text
===================== APPLYING =====================

 [ OK  ]  nginx.service            started in 0.42s
 [ OK  ]  c-relay.service          started in 1.14s
 [ ... ]  fips.service             starting...
 [     ]  (waiting)

Press Ctrl-C to abort remaining jobs (already-started services stay up).
```

Each row shows one of:
- `[     ]` queued
- `[ ... ]` in progress (animated dot after 250 ms; ANSI cursor save/restore to re-render only the affected row)
- `[ OK  ]` finished successfully
- `[FAIL ]` finished with an error
- `[SKIP ]` not applied (e.g. dependency refused earlier)

After all jobs complete:

- All green → advance to Exit immediately (print `All services started. Handing control to login...` for 1 second, then the TUI exits and getty takes over).
- Any failures → advance to **ApplyError** screen.

### 3.4 Apply-error screen

```text
===================== PARTIAL FAILURE =====================

Some services did not start cleanly:

 [FAIL ]  ginxsom.service
          SubState: failed
          ExecMainStatus: 1
          Last log line: "/usr/local/bin/ginxsom/ginxsom-fcgi: socket bind failed"

Actions:
 V) View journal for failed services
 R) Retry failed services only
 B) Back to service menu
 I) Ignore and continue to login anyway

Select action: >
```

The journal view (`V`) shells out to `journalctl -u <unit> -n 50 --no-pager` via a pipe (we do not embed a log viewer). `less`-style paging is acceptable if available; otherwise we just print and prompt to continue.

## 4. Service manifest

The TUI does not hard-code the service list in C. It reads `/etc/n-os-tr/services.conf` at startup. This lets us add/remove services (especially tor, which lands later) without rebuilding the binary.

### 4.1 File format

A small line-based format, one record per service, fields tab-separated:

```
# key  unit                        label          default  deps
r      c-relay.service             c-relay        off      nginx.service
g      ginxsom.service             ginxsom        off      nginx.service
f      fips.service                fips           off      -
n      nginx.service               nginx          off      -
l      nostr-config-loader.service config-loader  off      -
w      nostr-config-writer.service config-writer  off      nostr-config-loader.service
s      ssh.service                 ssh            off      -
t      tor.service                 tor            off      -
```

Comments start with `#`. Blank lines ignored. Unknown fields ignored for forward compatibility. Multiple deps separated by `,`. A literal `-` means no deps.

Parsing happens in `stack/nostr-id-tui/src/services_conf.c`. On parse error the TUI falls back to a hardcoded minimal list (c-relay, ginxsom, fips, nginx) and logs the parse error to stderr (captured by journald, per the service unit's `StandardError=journal`).

### 4.2 Install location

Shipped in the chroot at [`iso/config/includes.chroot/etc/n-os-tr/services.conf`](../iso/config/includes.chroot/etc/n-os-tr/services.conf). Mode `0644`, owner `root:root`.

## 5. Control path: talking to systemd

### 5.1 Transport choice

The TUI needs to:
1. Query each unit's current state (`LoadState`, `ActiveState`, `SubState`).
2. Start and stop units.
3. Receive job-completion signals.

Two realistic options in a static musl C binary with no glibc:

| Option                                                         | Notes                                                                                       | Verdict             |
|----------------------------------------------------------------|---------------------------------------------------------------------------------------------|---------------------|
| **Shell out to `systemctl`** via `fork` + `execve`             | Simple. Exit codes are well-defined. Parse `show --property=ActiveState=`. No linking.       | **v1 default.**     |
| **Speak the D-Bus wire protocol directly** to `/run/dbus/system_bus_socket` | Avoids forking. Enables real-time `JobRemoved` signals. Needs a tiny D-Bus marshaller (~1 KLOC) or the vendored cjson-style lib. | Later optimization.  |
| **Link `libsystemd.a`**                                        | Would pull in a chunk of systemd source into Alpine build, and libsystemd isn't available as `-static` in Alpine. | Rejected.            |

v1 uses `systemctl`. The binary path is hardcoded to `/bin/systemctl` (present on every Debian live image — `systemctl` itself is part of `systemd` which is in base). The wrapper module is `stack/nostr-id-tui/src/systemd_client.c` and exposes:

```c
typedef enum {
    SVC_INACTIVE = 0,
    SVC_ACTIVATING,
    SVC_ACTIVE,
    SVC_DEACTIVATING,
    SVC_FAILED,
    SVC_UNKNOWN
} svc_state_t;

svc_state_t systemd_active_state(const char *unit);  /* systemctl show -p ActiveState */
int         systemd_start(const char *unit, int timeout_ms, char *err_out, size_t err_len);
int         systemd_stop (const char *unit, int timeout_ms, char *err_out, size_t err_len);
int         systemd_journal_tail(const char *unit, int n_lines, FILE *out); /* exec journalctl */
```

Each call spawns `systemctl` / `journalctl` with `posix_spawn` into a pipe, waits with a timeout, captures stdout/stderr, and returns.

### 5.2 Privilege

`nostr-id-tui.service` currently runs as root on `tty1` (see the service unit). That gives it direct `systemctl` privilege — no polkit rule needed. This matches the existing threat-model stance in [`plans/tui_login.md §6.1`](tui_login.md#61-etcsystemdsystemnostr-id-tuiservice): the TUI is already privileged because it owns the tty and talks to identity-agent over a root-only socket.

If we ever demote the TUI to run as the `nostr-id` user, we'll need `/etc/polkit-1/rules.d/50-n-os-tr-services.rules` granting that user the `org.freedesktop.systemd1.manage-units` action **only for the whitelisted units listed in `services.conf`**. That's future work.

### 5.3 Timeouts and cancellation

- Per-unit start/stop timeout: 30 s (covers nginx startup, fips mesh ready, etc.). Larger than any real service needs.
- TUI-level overall budget: 2 minutes. If exceeded, Apply falls through to ApplyError with any unfinished jobs marked `[TIME]` and the user is prompted.
- Ctrl-C in the Apply screen sends SIGINT to the current `systemctl` child, marks the remaining jobs `[SKIP ]`, and goes to ApplyError.

## 6. Source tree changes

Extending the layout declared in [`plans/tui_login.md §2.2`](tui_login.md#22-code-that-lives-in-this-repo-n_os_tr):

```
stack/nostr-id-tui/
├── src/
│   ├── main.c                       # now dispatches identity→services→exit
│   ├── screen_menu.c                # unchanged
│   ├── screen_enter.c               # unchanged
│   ├── screen_generate.c            # unchanged
│   ├── screen_import.c              # unchanged
│   ├── screen_bunker.c              # unchanged
│   ├── screen_services.c            # NEW: main service toggle screen
│   ├── screen_services_confirm.c    # NEW: confirm + apply screens
│   ├── services_conf.c              # NEW: parser for /etc/n-os-tr/services.conf
│   ├── systemd_client.c             # NEW: fork/exec systemctl + journalctl
│   ├── curses_helpers.c             # unchanged
│   ├── agent_client.c               # unchanged
│   └── memhygiene.c                 # unchanged
├── include/
│   └── n_os_tr_tui.h                # adds svc_* decls + ui_backend hooks
└── tests/
    ├── test_screen_services.c       # NEW: state-machine unit test
    ├── test_services_conf.c         # NEW: parser fuzz/table tests
    ├── test_systemd_client.c        # NEW: runs against a fake systemctl shim
    └── test_integration.sh          # extended to toggle a service in QEMU
```

No ncurses API leaks into the new state machines. All rendering goes through the same `ui_backend_t` abstraction established in [`plans/tui_login.md §7`](tui_login.md#7-per-flavor-abstraction). The Pi LCD port will get a stripped service menu for free.

## 7. Systemd preset policy

### 7.1 What "installed but not enabled" means

For each toggleable unit we ship:
- The unit file in `iso/config/includes.chroot/etc/systemd/system/<unit>` or (for distro-packaged units like `ssh`, `tor`, `nginx`) in the package itself.
- **No symlink in `multi-user.target.wants/`** — i.e. no `systemctl enable`.
- A preset file that explicitly forbids enabling at image-build time.

### 7.2 Preset file

New file: [`iso/config/includes.chroot/etc/systemd/system-preset/50-n-os-tr.preset`](../iso/config/includes.chroot/etc/systemd/system-preset/50-n-os-tr.preset):

```
# n-OS-tr service posture: only the identity/boot pipeline auto-starts.
# Everything else is explicitly disabled. The TUI turns them on at the
# user's request after identity load.
enable nostr-id.service
enable nostr-id-tui.service
enable n-os-tr-firstboot.service

disable c-relay.service
disable ginxsom.service
disable fips.service
disable nginx.service
disable nostr-config-loader.service
disable nostr-config-writer.service
disable ssh.service
disable tor.service
```

The `50-` prefix lands before Debian defaults (typically `90-*.preset`), so our `disable` lines override e.g. nginx's upstream preset-enable.

### 7.3 Rework of the enable-services hook

Current hook [`iso/config/hooks/live/0020-enable-services.hook.chroot`](../iso/config/hooks/live/0020-enable-services.hook.chroot) enables the services directly. Rework it to be explicit about the new posture:

```sh
#!/bin/sh
set -eu

# Enable the always-on trio
systemctl enable n-os-tr-firstboot.service
systemctl enable nostr-id.service
systemctl enable nostr-id-tui.service

# Explicitly disable everything else in the service manifest.
# Idempotent; safe to re-run. `systemctl disable` is a no-op for units
# that aren't enabled yet, which is what we want.
for unit in \
    c-relay.service \
    ginxsom.service \
    fips.service \
    nginx.service \
    nostr-config-loader.service \
    nostr-config-writer.service \
    ssh.service \
    tor.service ; do
    systemctl disable "$unit" 2>/dev/null || true
done

# Run the preset policy for anything we didn't list above (defensive).
systemctl preset-all --preset-mode=enable-only 2>/dev/null || true
```

### 7.4 Package-list additions

`tor` is new to the image. Add to [`iso/config/package-lists/base.list.chroot`](../iso/config/package-lists/base.list.chroot):

```
tor
```

No other new packages: nginx, openssh-server, ca-certificates, systemd, etc. are already pulled.

## 8. Interaction with identity modes

### 8.1 Normal, Bunker, and Amnesia modes

- **Normal (mnemonic loaded)**: full menu, full defaults as in §1.1.
- **Bunker mode**: services that require a raw local nsec are **disabled in the menu** (greyed out with `[ n/a ]` in the state column). Those are:
  - `c-relay` — admin key comes from identity-agent via NIP-46 signing for event-based config only; for the admin nsec path the service fails. The menu shows `[ n/a ]` with a tooltip line "requires local key; bunker mode cannot provide".
  - `ginxsom` — same. Key material is needed at process start (`--server-privkey` in [`ginxsom.service`](../iso/config/includes.chroot/etc/systemd/system/ginxsom.service)).
  - `fips` — same.
  - `nginx`, `config-loader`, `config-writer`, `ssh`, `tor` — available in bunker mode.
- **Amnesia mode**: all services available, but `config-loader`/`config-writer` are explicitly disabled with label "amnesia session — config not persisted" (we don't pull anything from Nostr for an ephemeral identity).

The rules live in `services_conf.c` as a small matrix; the file format in §4.1 can be extended later with `modes=normal,bunker,amnesia` but v1 hardcodes the matrix in C.

### 8.2 Re-entry

A future option from the Services menu: `Q) Reset identity and return to identity menu`. Deferred — not in v1. For v1 the only path out of Services is `C` (commit) or `X` (skip), both of which lead to Exit.

## 9. Testing

### 9.1 Unit (in Alpine/musl builder)

- `test_services_conf` — feeds valid and malformed manifests, asserts parser produces correct structs and correct fallback on bad input.
- `test_systemd_client` — sets `$PATH` to a directory with a shim `systemctl` that records its argv and emits canned output; asserts our client parses `ActiveState=active|inactive|failed` correctly and times out when the shim sleeps.
- `test_screen_services` — drives the services state machine through a mock `ui_backend_t`; asserts:
  - Default state is all-off.
  - Toggling `R` then `G` then `R` ends with `G` on and `R` off.
  - Toggling `R` prompts for `N` enable; answering `y` enables both.
  - `Z` (reset) returns to defaults.
  - `X` (skip) goes directly to Apply with an empty plan.
  - `C` (commit) goes to Confirm.

### 9.2 Integration (QEMU)

Extend [`stack/nostr-id-tui/tests/test_integration.sh`](../stack/nostr-id-tui/tests/test_integration.sh) with a new scripted run:
1. Boot the ISO.
2. At the identity screen, feed the Trezor test-vector mnemonic.
3. At the services screen, press `N` (enable nginx), then `C`, then `Y`.
4. After the TUI exits and getty appears, log in as the default user.
5. Assert `systemctl is-active nginx` = `active` and `systemctl is-active c-relay` = `inactive`.

### 9.3 Smoketest extensions

Extend [`iso/config/includes.chroot/usr/local/bin/n-os-tr-smoketest`](../iso/config/includes.chroot/usr/local/bin/n-os-tr-smoketest) to also verify:

- Preset posture at **image build time**: for each toggleable unit, `systemctl is-enabled <unit>` returns `disabled`.
- The manifest file `/etc/n-os-tr/services.conf` exists and parses (re-invoke the parser from a test helper).
- `/bin/systemctl` is present (the TUI depends on it).

## 10. Non-persistence and the future Nostr slot

v1: selections are session-local. Reboot → all services back to off, user re-selects.

Future: one new entry in the manifest scheme from [`plans/nostr_config_projection.md §2.1`](nostr_config_projection.md#21-slots):

```json
{
  "name": "n-os-tr/services",
  "path": "/run/nostr-id/services.state.json",
  "mode": "0600",
  "encrypted": true
}
```

Content schema (draft):

```json
{
  "version": 1,
  "enabled": ["nginx.service", "c-relay.service", "fips.service"]
}
```

When a session starts, if a services slot exists, the TUI pre-loads the toggle state from it. User still has to press `C` to commit — we don't auto-apply without the user seeing the menu. This preserves the "the user decides when to turn them on" rule.

Out of scope for v1 implementation.

## 11. Failure modes

| Failure                                         | Behavior                                                                 |
|-------------------------------------------------|--------------------------------------------------------------------------|
| `/etc/n-os-tr/services.conf` missing or unparseable | Log to stderr, fall back to hardcoded minimal list (c-relay, ginxsom, fips, nginx). |
| `/bin/systemctl` missing                        | FatalError (as in [`plans/tui_login.md §9`](tui_login.md#9-failure-modes-summary)). The ISO is malformed. |
| Unit start returns non-zero                     | Row shows `[FAIL ]`, ApplyError screen is reached after all jobs settle.  |
| Unit start hangs past 30 s                      | Timeout, row shows `[TIME]`, ApplyError.                                 |
| User hits Ctrl-C during Apply                   | SIGINT to current `systemctl`, remaining jobs `[SKIP ]`, ApplyError.     |
| Bunker mode, user tries to toggle `c-relay`     | Toggle is ignored; status line shows "requires local key; bunker mode".  |
| User toggles all off and presses Commit         | Apply plan is empty; TUI prints `No services requested. Handing control to login...` and exits. |
| Identity agent dies during service menu         | We're not calling the agent during this phase — selections are in TUI memory only. User is unaffected until they finish service menu and the TUI exits; if the agent is dead when services start, those services will fail on their own dependency chain. |

## 12. Docs to update

- [`plans/tui_login.md`](tui_login.md) — add a one-line reference in §3.1 to the new `Services` state after `SendAgent`; add `screen_services.c`, `screen_services_confirm.c`, `services_conf.c`, `systemd_client.c` to the source tree listing in §2.2; update §13 implementation order with the service-menu steps.
- [`plans/iso_architecture.md`](iso_architecture.md) — update §4 boot sequence to show the identity+services TUI phase before the parallel `fips/c-relay/ginxsom/nginx` block, and note that those services are started by the TUI, not auto-started by systemd.
- [`iso/config/includes.chroot/usr/share/doc/n-os-tr/README`](../iso/config/includes.chroot/usr/share/doc/n-os-tr/README) — describe the new login→services flow for end users.
- [`docs/RUNNING.md`](../docs/RUNNING.md) — add a section showing the service menu and how to toggle.

## 13. Implementation order (Phase 3a.2)

Small, independently verifiable steps. Each one leaves the repo in a runnable state.

1. **Land the preset file and rework the enable hook.** After this commit, a fresh ISO boots to a system where only `nostr-id`, `nostr-id-tui`, and `firstboot` are enabled; every other service is present on disk but inactive. Verify with `systemctl list-unit-files --state=disabled` in a QEMU boot.
2. **Add the service manifest file.** Ship `/etc/n-os-tr/services.conf` with the v1 table from §1.1. No TUI change yet; verifies that the file reaches the image.
3. **Add `tor` to the base package list.** Confirm the built ISO includes `tor.service` as a disabled unit.
4. **Implement `services_conf.c` + tests.** Parser plus a 40-line sanity test in the Alpine builder.
5. **Implement `systemd_client.c` + tests.** Fork/exec + pipe capture + shim-based test.
6. **Implement `screen_services.c` state machine + `test_screen_services`.** No rendering yet — verifies transitions against a mock backend.
7. **Render the services screen in `ui_backend_curses.c`.** First visible new screen.
8. **Implement `screen_services_confirm.c` — confirm/apply/apply-error screens.** Wire them to `systemd_client`.
9. **Hook into `main.c`.** After `agent_client` returns from `load_mnemonic`/`load_bunker`/`load_amnesia` with ok, transition to the services screen. Skip the screen entirely if `/etc/n-os-tr/services.conf` is missing *and* fallback is disabled via a `--no-services` flag (useful for tests that want only the identity flow).
10. **Extend the QEMU integration test.** Script a toggle of one service, assert post-login that the chosen service is active and the others are not.
11. **Extend the smoketest.** Add the preset-state check.
12. **Doc passes.** Update [`plans/tui_login.md`](tui_login.md), [`plans/iso_architecture.md`](iso_architecture.md), [`docs/RUNNING.md`](../docs/RUNNING.md), and the in-ISO README.

## 14. Open questions

- **`tor.service` posture out of the box.** We ship the distro default `torrc` (SOCKS 9050 only, no relay). Do we want a second unit drop-in that enables the hidden-service onion for nginx when the user turns both on? Probably yes, as a follow-up; keep v1 minimal.
- **GUI flavor.** Image flavor with a GUI will run the TUI on a text VT during early boot. Does it also allow the user to re-run the services menu later from the GUI? Probably yes via a GTK wrapper around the same `systemd_client` module, but that's post-v1.
- **Per-user vs system.** All services here are system-level. "Which user's `ssh/authorized_keys` does `ssh` serve" is answered by the config-loader slot scheme. No per-user service toggles in v1.
- **Should toggling the menu's state affect *default* behavior for future reboots?** Today: no; v1 is stateless. A future Nostr slot (§10) could record the last committed state and let the TUI show it as a "remembered" default, but that's explicit future work.
