mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
fix: stop the home screen's tmux scan in a hidden tab (release review)
The poll had no hidden-tab gate, and the local scan's 5 s cache sits under the 8 s interval, so a single tab never hit it: every tick was a full socket sweep (1.1 s and 772 forks on a box with 212 sockets) for a list nobody was reading. `_foreignPollTick()` is now the one gate for the first load, the timer and the return to a visible tab. The `visibilitychange` listener is stored and removed in `stopForeignPolling()`, since this runs on every trip to the home screen. Checked in real chromium with document.hidden stubbed: 2 polls in 9.5 s visible, 0 in 17.5 s hidden, 1 on return, and a tab opened in the background makes none until it is shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
37dfb73b1e
commit
9714ee79e8
3 files changed
+108
-9
No files matched your search
+1
-1
@@ -46,7 +46,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
|
||||
|
||||
**Desktop home tab rail** (`home-sessions.js`, desktop only): the welcome overlay's left gutter carries the open tabs as a rail docked flush left, full height, in overview order, each row showing `created … · <state> <duration>` from `_mobileOverviewSince()`; state classification is reused from mobile-overview.js (so it loads after it). ⚠️ The number badge is the Alt+1..9 tab-strip index, never renumber it to row position. ⚠️ The width gate lives in two places that must stay equal: `HOME_SESSIONS_MIN_WIDTH` (1180) and a `max-width: 1179px` media query. ⚠️ `.home-sessions[hidden]` must re-assert `display: none`. ⚠️ Size all children in `em` off the one `clamp()` knob, never `rem`/px. Age stamps tick in place (`_tickHomeSessionsTimes()`), never by re-render. Test: `test/home-sessions.test.ts`. → [architecture-invariants#desktop-home-tab-rail](../../docs/architecture-invariants.md#desktop-home-tab-rail)
|
||||
|
||||
**Opened outside Codeman** (`foreign-sessions.js`, both home screens): the tmux sessions a human started, listed from `GET /api/mux/foreign` (root CLAUDE.md, Adopted tmux sessions). ONE renderer, `renderForeignSessions(container)`, fills the welcome block and the phone overview's, and every row goes through `textContent`, since a session name and a command line come from someone else's process. ⚠️ It polls only while a home screen is up (`startForeignPolling`/`stopForeignPolling` from `showWelcome()`/`hideWelcome()`), and the poll is local only: the docker and ssh scan costs one connection per target, so it runs once on the toggle and again on Rescan, and its rows and notes are kept apart (`_foreignWideSessions`, `_foreignWideNotes`) or the next local poll would wipe them. ⚠️ A 403 is a non-admin in multi-user mode, an answer and not a failed poll: `_foreignRefused` stops the polling and shows no block. ⚠️ `closeSession()` re-reads the list after an adopted tab's DELETE, since the home screen's own load races it and the row would say `Go to tab` until the next poll. ⚠️ An adopted tab has no kill outcome to offer, so the close dialog hides the red button and always tears the wrapper down. ⚠️ `session.remote` and `session.docker` are undefined on an adopted session, so a check that hides a row for those two usually needs `session.adopt` as well. Tests: `test/foreign-sessions-ui.test.ts`, `test/foreign-sessions-i18n.test.ts` (harvests every string from the real module).
|
||||
**Opened outside Codeman** (`foreign-sessions.js`, both home screens): the tmux sessions a human started, listed from `GET /api/mux/foreign` (root CLAUDE.md, Adopted tmux sessions). ONE renderer, `renderForeignSessions(container)`, fills the welcome block and the phone overview's, and every row goes through `textContent`, since a session name and a command line come from someone else's process. ⚠️ It polls only while a home screen is up (`startForeignPolling`/`stopForeignPolling` from `showWelcome()`/`hideWelcome()`) and the tab is visible (`_foreignPollTick()` is the one gate, and `visibilitychange` catches up; the cache TTL sits under the poll interval, so one tab's every tick is a full socket sweep), and the poll is local only: the docker and ssh scan costs one connection per target, so it runs once on the toggle and again on Rescan, and its rows and notes are kept apart (`_foreignWideSessions`, `_foreignWideNotes`) or the next local poll would wipe them. ⚠️ A 403 is a non-admin in multi-user mode, an answer and not a failed poll: `_foreignRefused` stops the polling and shows no block. ⚠️ `closeSession()` re-reads the list after an adopted tab's DELETE, since the home screen's own load races it and the row would say `Go to tab` until the next poll. ⚠️ An adopted tab has no kill outcome to offer, so the close dialog hides the red button and always tears the wrapper down. ⚠️ `session.remote` and `session.docker` are undefined on an adopted session, so a check that hides a row for those two usually needs `session.adopt` as well. Tests: `test/foreign-sessions-ui.test.ts`, `test/foreign-sessions-i18n.test.ts` (harvests every string from the real module).
|
||||
|
||||
**Home-screen session order** (`CodemanSessionOrder` in constants.js, pure): BOTH home screens (phone overview, desktop rail) must order rows through this ONE comparator. Rank `needs` → `error` → `waiting` → `working` → `idle` → `done`. ⚠️ The tiebreak flips: states a session is still IN sort oldest-first, states it has STOPPED sort newest-first. ⚠️ The running group keys off `lastSubmitAt`, never `lastActivityAt` (a working pane repaints constantly). ⚠️ A 0 stamp means unknown and sorts last within its state. Final tiebreak is `orderIndex`, so the list never shuffles. The tab strip itself is NOT sorted by this. Test: `test/session-overview-order.test.ts`. → [architecture-invariants#home-screen-session-order](../../docs/architecture-invariants.md#home-screen-session-order)
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
* tab keeps sweeping tmux sockets forever. `startForeignPolling` /
|
||||
* `stopForeignPolling` are called from `showWelcome`/`hideWelcome`.
|
||||
*
|
||||
* A tab hidden while it shows the home screen skips its ticks as well. The TTL sits
|
||||
* under the poll interval, so one tab never hits the cache: every tick is a full
|
||||
* sweep, one fork or more per socket, for a list nobody is reading. It catches up
|
||||
* on `visibilitychange`, and the cache is what bounds a flurry of tab switches.
|
||||
*
|
||||
* ⚠️ Docker and remote locations are NOT in that poll. Each costs a `docker exec`
|
||||
* or a full ssh handshake per target, each with a ~12s timeout, so riding them
|
||||
* along on an 8s tick stacks connections on a slow host, the one cost the
|
||||
@@ -78,11 +83,14 @@ Object.assign(CodemanApp.prototype, {
|
||||
*/
|
||||
startForeignPolling() {
|
||||
if (this._foreignPollTimer || this._foreignRefused) return;
|
||||
void this.loadForeignSessions();
|
||||
this._foreignPollTimer = setInterval(() => {
|
||||
// Local only: see @fileoverview. The wide scan never rides the timer.
|
||||
void this.loadForeignSessions();
|
||||
}, this._foreignPollMs || FOREIGN_POLL_FALLBACK_MS);
|
||||
this._foreignPollTick();
|
||||
this._foreignPollTimer = setInterval(
|
||||
() => this._foreignPollTick(),
|
||||
this._foreignPollMs || FOREIGN_POLL_FALLBACK_MS
|
||||
);
|
||||
// Stored, unlike a poller made once: this runs on every trip to the home screen.
|
||||
this._foreignOnVisible = () => this._foreignPollTick();
|
||||
document.addEventListener('visibilitychange', this._foreignOnVisible);
|
||||
},
|
||||
|
||||
/** Stop polling. MUST run on leaving the home screen, see @fileoverview. */
|
||||
@@ -90,6 +98,15 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (!this._foreignPollTimer) return;
|
||||
clearInterval(this._foreignPollTimer);
|
||||
this._foreignPollTimer = null;
|
||||
document.removeEventListener('visibilitychange', this._foreignOnVisible);
|
||||
this._foreignOnVisible = null;
|
||||
},
|
||||
|
||||
/** One poll, and the one gate every poll passes: a hidden tab skips it, see @fileoverview. */
|
||||
_foreignPollTick() {
|
||||
if (document.hidden) return;
|
||||
// Local only: see @fileoverview. The wide scan never rides the timer.
|
||||
void this.loadForeignSessions();
|
||||
},
|
||||
|
||||
/** Whether the user asked for the expensive docker/remote scan too. */
|
||||
|
||||
Reference in new issue
Block a user