diff --git a/controller/docs/operation-model-roadmap.svg b/controller/docs/operation-model-roadmap.svg
index 8988cf1e..7258f2fe 100644
--- a/controller/docs/operation-model-roadmap.svg
+++ b/controller/docs/operation-model-roadmap.svg
@@ -381,33 +381,44 @@
Finding 2 closed. A per-poll heartbeat ages a dead session out in
~30s; the refusal names the holder. In-memory, not 4897's record.
-SPANS THE BOARD — real work with no ticket, and the small things
-
-
-NO TICKET YET
-A finished run leaves no evidence it happened
-A LIVE run is meant to be left alone, so the user usually arrives after it ended and finds a bare “3 Done”. The rate is dropped at completion by design — rateSuffix is called only from the not-complete branch — and
-“838 MB · 32 MB/s · 4 min” is the only thing that tells a real download from a no-op. Courses can do it today (KolibriSeedState has bytes). ZIM has no clock at all. Books has neither bytes nor speed. The only start
-timestamp in main/ is KolibriSeedRepository.startedAtMs, private and unreachable from the row renderer. Also here: the maps card cannot show “adding content” — its progress belongs to the module queue.
-
-
-NO TICKET YET
-Replacing the system, and the installer that does it — no ticket on purpose
-Moved out of ADFA-4758, which had absorbed it. Neither item is a fix: they are capability the app does not have, on top of a flow that works. Recorded here so they are picked up on their own terms.
-• Two separately invoked installers. One ACTION_START becomes five pipelines through four booleans set by three unrelated senders.
-• Content-only backup, so a reinstall at another tier can keep what the user added. From zero, the box wipes and rewrites.
-Not blocked by this: the tier picker reports success while doing nothing when a rootfs directory exists — implemented and wrong today, so it is a bug with its own ticket.
-
-
-CARRIED
-Too small for a card, recorded so they are met on purpose
-• The Books “downloads” link is the last fragment hosting itself. Needs an index that can be opened with nothing in flight.
-• The three live REST streams can still start together: one provisioner drains asynchronously, so the others see no session yet.
-• EnvironmentLock.Owner.MODULE is never acquired, and hasUnfinishedWork is duplicated between EnvironmentLock and PendingContent (orphan TODO on a closed ticket).
-• The build's own native-artifact download has no retry: app/build.gradle:362 calls ant.get once, so a dropped socket on a 15 MB fetch fails CI outright.
- Seen 13 Aug — SocketException mid-ZIP. ant.get takes retries and maxtime, so it is one line. Same fault ADFA-4893 names, one layer up in the build.
-• DONE (ADFA-5152): a clone-receive reopened during a live transfer now lands on the progress screen, not the Send/Receive fork — CloneFragment re-binds the RECEIVE side
- symmetrically. Helps while the process is alive; a hard kill is still the durability problem (5146 family).
+
+
+ADFA-5192 · DONE
+Legacy tabbed UI retired
+MainActivity + the invisible pager, gone.
+
+Terminal re-homed to TerminalActivity
+
+4 fragments + carved controllers deleted
+
+Dead companion leg removed (ADFA-5144)
+SPANS THE BOARD — real work with no ticket, and the small things
+
+
+NO TICKET YET
+A finished run leaves no evidence it happened
+A LIVE run is meant to be left alone, so the user usually arrives after it ended and finds a bare “3 Done”. The rate is dropped at completion by design — rateSuffix is called only from the not-complete branch — and
+“838 MB · 32 MB/s · 4 min” is the only thing that tells a real download from a no-op. Courses can do it today (KolibriSeedState has bytes). ZIM has no clock at all. Books has neither bytes nor speed. The only start
+timestamp in main/ is KolibriSeedRepository.startedAtMs, private and unreachable from the row renderer. Also here: the maps card cannot show “adding content” — its progress belongs to the module queue.
+
+
+NO TICKET YET
+Replacing the system, and the installer that does it — no ticket on purpose
+Moved out of ADFA-4758, which had absorbed it. Neither item is a fix: they are capability the app does not have, on top of a flow that works. Recorded here so they are picked up on their own terms.
+• Two separately invoked installers. One ACTION_START becomes five pipelines through four booleans set by three unrelated senders.
+• Content-only backup, so a reinstall at another tier can keep what the user added. From zero, the box wipes and rewrites.
+Not blocked by this: the tier picker reports success while doing nothing when a rootfs directory exists — implemented and wrong today, so it is a bug with its own ticket.
+
+
+CARRIED
+Too small for a card, recorded so they are met on purpose
+• The Books “downloads” link is the last fragment hosting itself. Needs an index that can be opened with nothing in flight.
+• The three live REST streams can still start together: one provisioner drains asynchronously, so the others see no session yet.
+• EnvironmentLock.Owner.MODULE is never acquired, and hasUnfinishedWork is duplicated between EnvironmentLock and PendingContent (orphan TODO on a closed ticket).
+• The build's own native-artifact download has no retry: app/build.gradle:362 calls ant.get once, so a dropped socket on a 15 MB fetch fails CI outright.
+ Seen 13 Aug — SocketException mid-ZIP. ant.get takes retries and maxtime, so it is one line. Same fault ADFA-4893 names, one layer up in the build.
+• DONE (ADFA-5152): a clone-receive reopened during a live transfer now lands on the progress screen, not the Send/Receive fork — CloneFragment re-binds the RECEIVE side
+ symmetrically. Helps while the process is alive; a hard kill is still the durability problem (5146 family).
a real figure
@@ -417,30 +428,22 @@
same launch file — sequence them
-
-
-NO TICKET YET
-The legacy UI is invisible but still running — open a ticket for it
-Found while writing Cancel (ADFA-5119). The launcher goes SplashActivity → LibraryActivity, and the only door left into the legacy MainActivity is Settings → “Terminal (Debian)”, which passes
-EXTRA_TERMINAL_ONLY so ADFA-4987 keeps the old dashboard hidden behind the terminal sheet. But MainActivity.onCreate builds MainPagerAdapter at :292 and only learns it is terminal-only at :344 —
-so DeployFragment is still constructed and still observes InstallProgressRepository from behind that sheet. Not dead code: invisible code that still runs, which is why its phase switch has to be kept in step.
-The one leak a user can reach: TerminalSessionService:98 opens MainActivity WITHOUT that extra, revealing the whole legacy dashboard with a working install button. Two lines close it — the missing extra, and not building the pager in terminal-only mode. Deleting DeployFragment is the larger piece behind it.
-SESSION 15 AUG — WHAT LANDED, AND WHAT IT SURFACED
-
-
-CLONE / STATE-SPINE ARC
-The receive/clone dead-ends closed — and the debt the fixes exposed
-Landed and merged: ADFA-5143 (a clone in flight has a name and a way back), 5147 (a damaged system reads as damaged), 5150 (systemless Connect/Send route to Recover), 5151
-(confine the receive loop to the recover hub), 5152 (a reopen lands on progress, not the fork), 5154 (Connect + Clone Send redesigned to stacked / 2-page QR), 5157 (shared QrSection),
-5155 (receive success waits for services before Home, never a fixed timer). Also landed: ADFA-5158 (get-app QR waits for the hotspot AP IP by polling, not a one-shot; broadened detection).
-FOLLOW-UPS WORTH A TICKET
-
-Shared "poll until a resource is ready" utility (a ServiceReadyGate). The same Handler-reschedule + in-flight guard + teardown now lives in three places — SetupProgressActivity.readyPoll,
-the 5155 receive-exit wait, and the 5158 AP-IP poll — with a fourth copy one bug away. Extract one loop (and a services-then-redirect gate on top for the first two).
-
-ADFA-5146 (finding 2, the release blocker) — LANDED, device-verified. Expires on LAST PROGRESS, not a start timestamp: the 5155/5158 lesson honored — a wall clock must never condemn a
-legitimately long op, so a heartbeat the live service refreshes and a dead one lets go cold ages a killed session out in ~30s, across the three content sources (the module queue keeps its own signal). The naming half is currentHolder + BusyMessage (six gates), and isHeld now derives from currentHolder — one source of truth.
-
-Generalize "which IP does the peer reach me at": peerReachableIp + NetworkInterfaces pass-2 (up, non-loopback, site-local, not Wi-Fi/VPN) replaced a hardcoded 192.168.49.1. One robust
-source belongs wherever a host advertises itself — the get-app URL, the copy daemon, the transfer QR — instead of a guess that only some OEMs happen to match.
+SESSION 15 AUG — WHAT LANDED, AND WHAT IT SURFACED
+
+
+CLONE / STATE-SPINE ARC
+The receive/clone dead-ends closed — and the debt the fixes exposed
+Landed and merged: ADFA-5143 (a clone in flight has a name and a way back), 5147 (a damaged system reads as damaged), 5150 (systemless Connect/Send route to Recover), 5151
+(confine the receive loop to the recover hub), 5152 (a reopen lands on progress, not the fork), 5154 (Connect + Clone Send redesigned to stacked / 2-page QR), 5157 (shared QrSection),
+5155 (receive success waits for services before Home, never a fixed timer). Also landed: ADFA-5158 (get-app QR waits for the hotspot AP IP by polling, not a one-shot; broadened detection).
+FOLLOW-UPS WORTH A TICKET
+
+Shared "poll until a resource is ready" utility (a ServiceReadyGate). The same Handler-reschedule + in-flight guard + teardown now lives in three places — SetupProgressActivity.readyPoll,
+the 5155 receive-exit wait, and the 5158 AP-IP poll — with a fourth copy one bug away. Extract one loop (and a services-then-redirect gate on top for the first two).
+
+ADFA-5146 (finding 2, the release blocker) — LANDED, device-verified. Expires on LAST PROGRESS, not a start timestamp: the 5155/5158 lesson honored — a wall clock must never condemn a
+legitimately long op, so a heartbeat the live service refreshes and a dead one lets go cold ages a killed session out in ~30s, across the three content sources (the module queue keeps its own signal). The naming half is currentHolder + BusyMessage (six gates), and isHeld now derives from currentHolder — one source of truth.
+
+Generalize "which IP does the peer reach me at": peerReachableIp + NetworkInterfaces pass-2 (up, non-loopback, site-local, not Wi-Fi/VPN) replaced a hardcoded 192.168.49.1. One robust
+source belongs wherever a host advertises itself — the get-app URL, the copy daemon, the transfer QR — instead of a guess that only some OEMs happen to match.