diff --git a/TERMIX_MIGRATION.md b/TERMIX_MIGRATION.md
index a64439d..52666de 100644
--- a/TERMIX_MIGRATION.md
+++ b/TERMIX_MIGRATION.md
@@ -45,11 +45,13 @@ Rationale for splitting: 1a alone is a real, useful terminal (matches what `/ter
**Status:**
- ✅ **Phase 1a — done.** `/terminal` is a real interactive SSH terminal: `backend/src/routes/terminal.ts` (WebSocket, connect/input/resize/disconnect over `ssh2`), `backend/src/db/secrets.ts` (shared secret loader), `src/pages/Terminal.tsx` (xterm.js + host picker, reuses ArchNest's existing SSH integrations — no duplicate host table). Verified end-to-end against a real test SSH server. No jump hosts, no tabs/split panes, no OPKSSH, no tmux monitor yet — see 1b/1c below.
-- 🟡 **Phase 1b — in progress.** Done so far:
+- ✅ **Phase 1b — done.**
- **Jump-host chaining**: an SSH integration's config can carry `jumpHostIntegrationId` referencing another SSH integration. `backend/src/routes/terminal.ts` connects to the jump host first, opens a `forwardOut()` channel to the real target, and connects the target `Client` over that channel (single-hop; mirrors Termix's core mechanism without its multi-hop/credential-sharing complexity). Verified end-to-end with two real test SSH servers (one as jump, one as target).
- **Host-key verification (TOFU)**: new `ssh_host_keys` table (`backend/src/db/index.ts`) stores a SHA-256 fingerprint per SSH integration on first successful connect; subsequent connects are rejected if the fingerprint changes, via `ssh2`'s `hostVerifier` connect option. No interactive accept/reject-changed-key UI yet — first-use accept-and-store, hard-reject on mismatch. Verified both the accept-on-first-use and reject-on-mismatch paths against a real test server.
- **Settings UI for multiple SSH hosts**: `src/pages/Settings.tsx` previously could only show/edit one integration per type, which silently broke multi-host SSH. Added a dedicated `SshHostsSection` with its own per-host cards (Save/Test/Delete) and an "Add SSH Host" flow, including a `Jump Host` dropdown populated from the other configured SSH hosts.
- - Not yet done: tab system + up-to-4 split panes and terminal theme/font customization in `src/pages/Terminal.tsx` — that page is still single-session only.
+ - **Tabs + up to 4 split panes**: `src/pages/Terminal.tsx` rewritten around a `TerminalPane` component (one xterm + WebSocket connection each, reusable). Each tab holds 1/2/4 panes (single / split-2 / 2x2 grid); each pane connects independently to whichever SSH host is clicked while it's focused.
+ - **Terminal theme/font customization**: a preferences bar (theme preset, font size, font family) persisted to `localStorage` (`archnest-terminal-prefs`), applied per-pane on connect.
+ - Verified via a clean production build (`tsc -b && vite build`) — no real browser available in this environment to click through tabs/panes, so this is build/type verification only, not an interactive UI test.
- ⬜ Phase 1c — not started.
### Phase 2 — SSH Tunnels (NOT STARTED)
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
index f1c5ab7..c6ac2c4 100644
--- a/src/pages/Settings.tsx
+++ b/src/pages/Settings.tsx
@@ -398,7 +398,7 @@ function SshHostsSection() {
setDrafts((prev) => ({ ...prev, [id]: { ...prev[id], [fieldKey]: value } }))
}
- const fieldsWithJumpHost = (excludeId?: number): FieldDef[] => [
+ const fieldsWithJumpHost = (): FieldDef[] => [
...sshFields,
{ key: 'jumpHostIntegrationId', label: 'Jump Host (optional)' },
]
@@ -420,7 +420,7 @@ function SshHostsSection() {
setStatusMsg((prev) => ({ ...prev, [host.id]: '' }))
try {
const draft = drafts[host.id] ?? {}
- const { config, secrets } = buildPayload(fieldsWithJumpHost(host.id), draft)
+ const { config, secrets } = buildPayload(fieldsWithJumpHost(), draft)
const { integration } = await api.updateIntegration(host.id, { config, secrets })
setHosts((prev) => (prev ?? []).map((h) => (h.id === integration.id ? integration : h)))
setStatusMsg((prev) => ({ ...prev, [host.id]: 'Saved' }))
@@ -584,7 +584,7 @@ function SshHostsSection() {