/** * public/manage.ts — standalone Session Manager page (/manage.html). * * Lists the host's running server sessions (GET /live-sessions) and lets you * open one (?join=), kill one (DELETE /live-sessions/:id), or bulk-kill all * / only detached ones (DELETE /live-sessions[?detached=1]). Auto-refreshes. * Independent of the main app, so it works even when many sessions are open. */ interface LiveSession { id: string createdAt: number clientCount: number status: 'working' | 'waiting' | 'idle' | 'unknown' exited: boolean cwd: string | null cols: number rows: number } const REFRESH_MS = 3000 function relTime(ms: number): string { const s = Math.max(0, (Date.now() - ms) / 1000) if (s < 60) return `${Math.floor(s)}s` if (s < 3600) return `${Math.floor(s / 60)}m` if (s < 86400) return `${Math.floor(s / 3600)}h` return `${Math.floor(s / 86400)}d` } function statusGlyph(s: LiveSession['status']): string { return s === 'working' ? '⚙ working' : s === 'waiting' ? '⏳ waiting' : s === 'idle' ? '✓ idle' : '·' } async function fetchSessions(): Promise { try { const res = await fetch('/live-sessions') const data: unknown = await res.json() return Array.isArray(data) ? (data as LiveSession[]) : [] } catch { return [] } } const root = document.getElementById('manage-root') if (!root) throw new Error('#manage-root not found') let busy = false function el( tag: K, cls?: string, text?: string, ): HTMLElementTagNameMap[K] { const node = document.createElement(tag) if (cls) node.className = cls if (text !== undefined) node.textContent = text return node } async function killOne(id: string): Promise { if (busy) return busy = true try { await fetch(`/live-sessions/${id}`, { method: 'DELETE' }) } catch { /* best-effort */ } busy = false void render() } async function killBulk(detachedOnly: boolean): Promise { if (busy) return const label = detachedOnly ? 'all DETACHED sessions (no device watching)' : 'ALL sessions' if (!confirm(`Kill ${label}? Running shells/Claude will be terminated.`)) return busy = true try { await fetch(`/live-sessions${detachedOnly ? '?detached=1' : ''}`, { method: 'DELETE' }) } catch { /* best-effort */ } busy = false void render() } function row(s: LiveSession): HTMLElement { const r = el('div', 'mg-row') const main = el('div', 'mg-main') const top = el('div', 'mg-top') const name = el('span', 'mg-name', s.cwd ? (s.cwd.split('/').filter(Boolean).pop() ?? s.cwd) : s.id.slice(0, 8)) const watchers = el('span', s.clientCount > 0 ? 'mg-watch live' : 'mg-watch', `👁 ${s.clientCount}`) const status = el('span', `mg-status mg-${s.status}`, statusGlyph(s.status)) top.append(name, watchers, status) const meta = el('div', 'mg-meta', `${s.cwd ?? 'unknown dir'} · ${s.cols}×${s.rows} · ${relTime(s.createdAt)} old · ${s.id.slice(0, 8)}`) main.append(top, meta) const actions = el('div', 'mg-actions') const open = el('a', 'mg-open', 'Open ↗') as HTMLAnchorElement open.href = `/?join=${s.id}` const kill = el('button', 'mg-kill', 'Kill ✕') kill.addEventListener('click', () => void killOne(s.id)) actions.append(open, kill) r.append(main, actions) return r } async function render(): Promise { const sessions = await fetchSessions() root!.replaceChildren() const header = el('div', 'mg-header') header.append(el('div', 'mg-title', 'Session Manager')) const sub = el('div', 'mg-sub', `${sessions.length} session(s) running on the host`) header.append(sub) const bar = el('div', 'mg-bar') const back = el('a', 'mg-btn', '← Back to terminal') as HTMLAnchorElement back.href = '/' const refresh = el('button', 'mg-btn', '↻ Refresh') refresh.addEventListener('click', () => void render()) const killDetached = el('button', 'mg-btn warn', 'Kill detached') killDetached.addEventListener('click', () => void killBulk(true)) const killAll = el('button', 'mg-btn danger', 'Kill all') killAll.addEventListener('click', () => void killBulk(false)) bar.append(back, refresh, killDetached, killAll) header.append(bar) root!.append(header) if (sessions.length === 0) { root!.append(el('div', 'mg-empty', 'No sessions running. Open the terminal to start one.')) return } const list = el('div', 'mg-list') for (const s of sessions) list.append(row(s)) root!.append(list) } void render() setInterval(() => { if (!busy) void render() }, REFRESH_MS)