diff --git a/CLAUDE.md b/CLAUDE.md
index 14abd781..e233fd8a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -33,20 +33,6 @@ npx vitest run test/session.test.ts # Single file
npx vitest run -t "should create session" # By pattern
```
-### Test Files
-
-| File | Coverage |
-|------|----------|
-| `session.test.ts` | Session creation, PTY modes, token tracking |
-| `respawn-controller.test.ts` | State machine transitions, config updates |
-| `scheduled-runs.test.ts` | Timed runs, iteration cleanup |
-| `quick-start.test.ts` | Case creation + session startup |
-| `sse-events.test.ts` | Event broadcasting, client reconnection |
-| `integration-flows.test.ts` | Multi-step workflows |
-| `edge-cases.test.ts` | Error handling, boundary conditions |
-| `session-cleanup.test.ts` | Process termination, buffer management |
-| `pty-interactive.test.ts` | Terminal resize, input handling |
-
## Architecture
```
@@ -64,7 +50,7 @@ src/
├── types.ts # All TypeScript interfaces
├── web/
│ ├── server.ts # Fastify REST API + SSE + session restoration
-│ └── public/ # Static frontend files
+│ └── public/ # Static frontend files (vanilla JS, xterm.js)
└── templates/
└── claude-md.ts # CLAUDE.md generator for new cases
```
@@ -78,43 +64,19 @@ src/
### Key Components
-- **Session** (`src/session.ts`): Wraps Claude CLI as PTY subprocess. Two modes: `runPrompt(prompt)` for one-shot execution, `startInteractive()` for persistent terminal. Emits `output`, `terminal`, `message`, `completion`, `exit`, `idle`, `working`, `autoClear`, `clearTerminal` events. Maintains terminal buffer for reconnections. Includes buffer management for long-running sessions (12-24+ hours) with automatic trimming. Tracks input/output tokens and supports auto-clear at configurable threshold.
+- **Session** (`src/session.ts`): Wraps Claude CLI as PTY subprocess. Two modes: `runPrompt(prompt)` for one-shot, `startInteractive()` for persistent terminal. Emits `output`, `terminal`, `message`, `completion`, `exit`, `idle`, `working`, `autoClear`, `clearTerminal` events.
-- **TaskTracker** (`src/task-tracker.ts`): Detects Claude's background Task tool usage from JSON output. Builds a tree of parent-child task relationships. Emits `taskCreated`, `taskUpdated`, `taskCompleted`, `taskFailed` events. Used by Session to track background work.
+- **RespawnController** (`src/respawn-controller.ts`): State machine that keeps interactive sessions productive. Detects idle → sends update prompt → optionally `/clear` → optionally `/init` → repeats.
-- **RespawnController** (`src/respawn-controller.ts`): State machine that keeps interactive sessions productive. Detects idle → sends update prompt → optionally `/clear` → optionally `/init` → repeats. Configurable timeouts, prompts, and step toggles.
+- **ScreenManager** (`src/screen-manager.ts`): Manages GNU screen sessions for persistent terminals. Screens survive server restarts.
-- **RalphLoop** (`src/ralph-loop.ts`): Autonomous task assignment controller. Monitors sessions for idle state, assigns tasks from queue, detects completion via `PHRASE` markers. Supports time-aware loops with minimum duration.
-
-- **WebServer** (`src/web/server.ts`): Fastify server with REST API + SSE. Manages sessions, scheduled runs, respawn controllers, and case directories. Broadcasts all events to connected clients. Restores screen sessions on startup.
-
-- **ScreenManager** (`src/screen-manager.ts`): Manages GNU screen sessions for persistent terminals. Tracks screens in `~/.claudeman/screens.json`. Provides process stats (memory, CPU, children) and reconciliation for dead screens. Screens survive server restarts.
-
-### Type Definitions
-
-All TypeScript interfaces are centralized in `src/types.ts`:
-- `SessionState`, `TaskState`, `RalphLoopState` - Core state types
-- `RespawnConfig`, `AppConfig` - Configuration types
-- `ApiErrorCode`, `createErrorResponse()` - Consistent API error handling
-- Request/Response types for API endpoints (`CreateSessionRequest`, `QuickStartResponse`, etc.)
+- **WebServer** (`src/web/server.ts`): Fastify server with REST API + SSE. All endpoints under `/api/`. See file for full route list.
### Session Modes
-**One-Shot Mode** (`runPrompt(prompt)`):
-- Execute a single prompt and receive completion event
-- Used for scheduled runs and quick API calls
-- Session exits after prompt completes
-
-**Interactive Mode** (`startInteractive()`):
-- Persistent PTY terminal with full Claude CLI access
-- Supports terminal resize for proper formatting
-- Terminal buffer persisted for client reconnections
-- Works with RespawnController for autonomous cycling
-
-**Shell Mode** (`startShell()`):
-- Plain bash/zsh terminal without Claude
-- Useful for running commands alongside Claude sessions
-- Same PTY features (resize, buffer persistence)
+- **One-Shot** (`runPrompt(prompt)`): Execute single prompt, receive completion event, session exits
+- **Interactive** (`startInteractive()`): Persistent PTY terminal with resize support, buffer persistence
+- **Shell** (`startShell()`): Plain bash/zsh terminal without Claude
## Code Patterns
@@ -132,13 +94,11 @@ const msg = JSON.parse(cleanLine) as ClaudeMessage;
### PTY Spawn Modes
-**One-shot mode** (prompt execution with JSON output for token tracking):
```typescript
+// One-shot mode (JSON output for token tracking)
pty.spawn('claude', ['-p', '--dangerously-skip-permissions', '--output-format', 'stream-json', prompt], { ... })
-```
-**Interactive mode** (persistent terminal, tokens parsed from status line):
-```typescript
+// Interactive mode (tokens parsed from status line)
pty.spawn('claude', ['--dangerously-skip-permissions'], { ... })
```
@@ -146,44 +106,10 @@ pty.spawn('claude', ['--dangerously-skip-permissions'], { ... })
Session detects idle by watching for prompt character (`❯` or `\u276f`) and waiting 2 seconds without activity. RespawnController uses the same patterns plus spinner characters to detect working state.
-### Long-Running Session Support
+### Token Tracking
-Sessions are optimized for 12-24+ hour runs with automatic buffer management:
-
-**Buffer Limits:**
-- Terminal buffer: 5MB max, trims to 4MB when exceeded
-- Text output: 2MB max, trims to 1.5MB when exceeded
-- Messages: 1000 max, keeps most recent 800 when exceeded
-- Line buffer: 64KB max with 100ms periodic flush (prevents unbounded growth from long lines)
-- Completed tasks: 100 max in TaskTracker (auto-removes oldest)
-- Respawn terminal buffer: 1MB max, trims to 512KB
-
-**Performance Optimizations:**
-- Server-side terminal batching at 60fps (16ms intervals)
-- SSE event batching: `session:output` at 50ms, `task:updated` at 100ms
-- Client-side requestAnimationFrame batching for smooth rendering
-- Frontend `renderSessionTabs()` debounced at 100ms
-- Parallel screen stats fetching via `Promise.all()`
-- Configurable xterm scrollback (default 5000 lines)
-- Buffer statistics available via session details for monitoring
-
-**Automatic Cleanup:**
-- Scheduled runs auto-deleted after 1 hour of completion
-- Sessions cleaned up after each scheduled run iteration (prevents leaks)
-
-**Buffer Stats Response:**
-```typescript
-{
- bufferStats: {
- terminalBufferSize: number; // Current terminal buffer size in bytes
- textOutputSize: number; // Current text output size in bytes
- messageCount: number; // Number of parsed messages
- maxTerminalBuffer: number; // Max allowed terminal buffer
- maxTextOutput: number; // Max allowed text output
- maxMessages: number; // Max allowed messages
- }
-}
-```
+- **One-shot mode**: Uses `--output-format stream-json` for detailed token usage from JSON
+- **Interactive mode**: Parses tokens from Claude's status line (e.g., "123.4k tokens"), estimates 60/40 input/output split
### Respawn Controller State Machine
@@ -191,174 +117,48 @@ Sessions are optimized for 12-24+ hour runs with automatic buffer management:
WATCHING → SENDING_UPDATE → WAITING_UPDATE → SENDING_CLEAR → WAITING_CLEAR → SENDING_INIT → WAITING_INIT → WATCHING
```
-Default config (`RespawnConfig` in `src/types.ts`):
-- `idleTimeoutMs`: 5000 (5s after prompt)
-- `updatePrompt`: "update all the docs and CLAUDE.md"
-- `interStepDelayMs`: 1000 (1s between steps)
-- `sendClear`: true (send /clear after update)
-- `sendInit`: true (send /init after /clear)
-
-### Token Tracking & Auto-Clear
-
-Session tracks input/output tokens differently depending on mode:
-
-**One-shot mode (`runPrompt`)**: Uses `--output-format stream-json` to get JSON output with detailed token usage from `msg.message.usage.input_tokens` and `output_tokens`.
-
-**Interactive mode (`startInteractive`)**: Parses tokens from Claude's status line display (e.g., "123.4k tokens"). Since only total is shown, estimates 60/40 input/output split.
-
-```typescript
-{
- tokens: {
- input: number; // Total input tokens used
- output: number; // Total output tokens used
- total: number; // Combined total
- },
- autoClear: {
- enabled: boolean; // Whether auto-clear is active
- threshold: number; // Token threshold (default 100000)
- }
-}
-```
-
-When enabled, auto-clear waits for idle state, sends `/clear`, and resets token counts.
-
### Screen Session Initialization
-GNU screen creates blank space at the top when initializing a session. This is handled after attaching to the screen:
+GNU screen creates blank space at top when initializing. After attaching:
+- **Claude sessions**: Clear buffer, emit `clearTerminal` event, client clears xterm
+- **Shell sessions**: Clear buffer, send `clear\n` command
-- **Claude sessions**: After 100ms, clears terminal buffer and emits `clearTerminal` event. Client receives `session:clearTerminal` via SSE and clears/resets its xterm.
-- **Shell sessions**: After 100ms, clears terminal buffer and sends `clear\n` command to the shell.
-
-### Tab Switching
-
-When switching between Claude session tabs, the terminal buffer may have been rendered at a different terminal size. To fix the "squished" display:
+### Tab Switching Fix
+When switching Claude session tabs, terminal may be rendered at wrong size. Fix sequence:
1. Clear and reset xterm
-2. Write the terminal buffer
+2. Write terminal buffer
3. Send resize to update PTY dimensions
-4. Send Ctrl+L (`\x0c`) to trigger Claude CLI to redraw at the correct size
-
-This only applies to Claude sessions (not shell sessions) since Claude CLI responds to Ctrl+L by redrawing its interface.
+4. Send Ctrl+L (`\x0c`) to trigger Claude CLI redraw
### SSE Events
All events broadcast to `/api/events` with format: `{ type: string, sessionId?: string, data: any }`.
-Event categories (prefixes): `session:`, `task:`, `respawn:`, `scheduled:`, `case:`, `init`. Key events include `session:idle`, `session:working`, `session:terminal`, `session:clearTerminal`, `session:completion`, `respawn:stateChanged`. See `src/web/server.ts` for the full event catalog.
-
-## API Endpoints
-
-REST API served by Fastify at `src/web/server.ts`. All endpoints are under `/api/`.
-
-**Sessions:**
-- `GET /api/sessions` - List all sessions
-- `POST /api/sessions` - Create session `{ workingDir, mode?, name? }`
-- `GET /api/sessions/:id` - Get session details (includes bufferStats)
-- `GET /api/sessions/:id/output` - Get text output buffer
-- `DELETE /api/sessions/:id` - Kill and remove session
-- `DELETE /api/sessions` - Kill all sessions
-- `PUT /api/sessions/:id/name` - Rename session `{ name }`
-- `POST /api/sessions/:id/interactive` - Start interactive Claude terminal
-- `POST /api/sessions/:id/shell` - Start shell terminal (no Claude)
-- `POST /api/sessions/:id/input` - Send input to session `{ input }`
-- `POST /api/sessions/:id/resize` - Resize terminal `{ cols, rows }`
-- `POST /api/sessions/:id/run` - Run one-shot prompt `{ prompt }`
-- `GET /api/sessions/:id/terminal` - Get terminal buffer
-
-**Respawn Controller:**
-- `GET /api/sessions/:id/respawn` - Get respawn state
-- `POST /api/sessions/:id/respawn/start` - Start respawn `{ config? }`
-- `POST /api/sessions/:id/respawn/stop` - Stop respawn
-- `PUT /api/sessions/:id/respawn/config` - Update config
-- `POST /api/sessions/:id/respawn/enable` - Enable on running session `{ config?, durationMinutes? }`
-- `POST /api/sessions/:id/auto-clear` - Configure auto-clear `{ enabled, threshold? }`
-
-**Cases & Quick Start:**
-- `GET /api/cases` - List cases in `~/claudeman-cases/`
-- `POST /api/cases` - Create case `{ name, description? }`
-- `GET /api/cases/:name` - Get case info
-- `POST /api/quick-start` - Create case + interactive session `{ caseName? }`
-
-**Scheduled Runs:**
-- `GET /api/scheduled` - List scheduled runs
-- `POST /api/scheduled` - Create scheduled run `{ prompt, workingDir?, durationMinutes }`
-- `GET /api/scheduled/:id` - Get run status
-- `DELETE /api/scheduled/:id` - Cancel run
-
-**Screen Management:**
-- `GET /api/screens` - List screen sessions with stats
-- `DELETE /api/screens/:sessionId` - Kill screen session
-- `POST /api/screens/reconcile` - Clean up dead screens
-- `POST /api/screens/stats/start` - Start resource monitoring
-- `POST /api/screens/stats/stop` - Stop resource monitoring
-
-**System:**
-- `GET /api/system/stats` - Get CPU and memory usage `{ cpu, memory: { usedMB, totalMB, percent } }`
-
-**Other:**
-- `GET /api/events` - SSE stream for real-time updates
-- `GET /api/status` - Full state snapshot
-- `GET /api/settings` - Get app settings
-- `PUT /api/settings` - Update settings
-- `POST /api/run` - Quick run prompt without creating persistent session `{ prompt, workingDir? }`
-
-## E2E Testing with agent-browser
-
-For UI testing, use [agent-browser](https://github.com/vercel-labs/agent-browser). A full E2E test plan is documented in `.claude/skills/e2e-test.md`.
-
-```bash
-# Setup
-npx agent-browser install # Download Chromium (one-time)
-npx tsx src/index.ts web & # Start server
-
-# Basic test flow
-npx agent-browser open http://localhost:3000
-npx agent-browser snapshot # Get accessibility tree with element refs
-npx agent-browser click @e5 # Click by element ref
-npx agent-browser find text "Run Claude" click # Or use semantic locators
-npx agent-browser screenshot /tmp/test.png
-npx agent-browser close
-```
-
-## Frontend
-
-The web UI (`src/web/public/`) uses vanilla JavaScript with:
-- **xterm.js**: Terminal emulator with configurable scrollback (default 5000 lines)
-- **xterm-addon-fit**: Auto-resize terminal to container
-- **Server-Sent Events**: Real-time updates from `/api/events`
-- **System Stats**: CPU and memory usage displayed in header (2s polling)
-- **No build step**: Static files served directly by Fastify
-
-Key files:
-- `app.js` - Main application logic, SSE handling, session management
-- `index.html` - Single page with embedded styles
-- `styles.css` - All CSS styles
-- Libraries loaded from CDN (xterm.js, addons)
+Event prefixes: `session:`, `task:`, `respawn:`, `scheduled:`, `case:`, `init`. Key events: `session:idle`, `session:working`, `session:terminal`, `session:clearTerminal`, `session:completion`, `respawn:stateChanged`.
## Adding New Features
### New API Endpoint
-
-1. Add types to `src/types.ts` (request/response interfaces)
-2. Add route in `src/web/server.ts` within the `buildServer()` function
-3. Follow existing patterns: use `createErrorResponse()` for errors
+1. Add types to `src/types.ts`
+2. Add route in `src/web/server.ts` within `buildServer()`
+3. Use `createErrorResponse()` for errors
### New SSE Event
-
-1. Add event type constant in `src/web/server.ts` (see `broadcast()` calls)
-2. Emit from appropriate component (Session, RespawnController, etc.)
-3. Handle in `src/web/public/app.js` `handleSSEEvent()` switch
+1. Emit from component via `broadcast()` in server.ts
+2. Handle in `src/web/public/app.js` `handleSSEEvent()` switch
### New Session Event
-
1. Add to `SessionEvents` interface in `src/session.ts`
-2. Emit in `src/session.ts` via `this.emit()`
+2. Emit via `this.emit()`
3. Subscribe in `src/web/server.ts` when wiring session to SSE
-4. Handle in `src/web/public/app.js` SSE event listener
+4. Handle in frontend SSE listener
## Notes
- State persists to `~/.claudeman/state.json` and `~/.claudeman/screens.json`
-- Cases are created in `~/claudeman-cases/` by default
-- Sessions are wrapped in GNU screen for persistence across server restarts
-- Tests use vitest with mocking via `vi.mock()` - no real Claude CLI spawned
+- Cases created in `~/claudeman-cases/` by default
+- Sessions wrapped in GNU screen for persistence across server restarts
+- Tests use vitest with `vi.mock()` - no real Claude CLI spawned
+- Long-running sessions (12-24+ hours) supported with automatic buffer trimming
+- E2E testing available via agent-browser (see `.claude/skills/e2e-test.md`)
diff --git a/src/web/public/app.js b/src/web/public/app.js
index 7d28bac1..507c7748 100644
--- a/src/web/public/app.js
+++ b/src/web/public/app.js
@@ -1470,6 +1470,92 @@ class ClaudemanApp {
this.toggleMonitorPanel();
}
+ // ========== Monitor Panel Detach & Drag ==========
+
+ toggleMonitorDetach() {
+ const panel = document.getElementById('monitorPanel');
+ const detachBtn = document.getElementById('monitorDetachBtn');
+
+ if (panel.classList.contains('detached')) {
+ // Re-attach to bottom
+ panel.classList.remove('detached');
+ panel.style.top = '';
+ panel.style.left = '';
+ panel.style.width = '';
+ panel.style.height = '';
+ if (detachBtn) {
+ detachBtn.innerHTML = '↗'; // Detach icon
+ detachBtn.title = 'Detach panel';
+ }
+ } else {
+ // Detach as floating window
+ panel.classList.add('detached');
+ panel.classList.add('open'); // Ensure it's visible
+ if (detachBtn) {
+ detachBtn.innerHTML = '↙'; // Attach icon
+ detachBtn.title = 'Attach panel';
+ }
+ // Setup drag functionality
+ this.setupMonitorDrag();
+ }
+ }
+
+ setupMonitorDrag() {
+ const panel = document.getElementById('monitorPanel');
+ const header = document.getElementById('monitorPanelHeader');
+
+ if (!panel || !header) return;
+
+ let isDragging = false;
+ let startX, startY, startLeft, startTop;
+
+ const onMouseDown = (e) => {
+ // Only drag from header, not from buttons
+ if (e.target.closest('button')) return;
+ if (!panel.classList.contains('detached')) return;
+
+ isDragging = true;
+ startX = e.clientX;
+ startY = e.clientY;
+ const rect = panel.getBoundingClientRect();
+ startLeft = rect.left;
+ startTop = rect.top;
+
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('mouseup', onMouseUp);
+ e.preventDefault();
+ };
+
+ const onMouseMove = (e) => {
+ if (!isDragging) return;
+
+ const dx = e.clientX - startX;
+ const dy = e.clientY - startY;
+
+ let newLeft = startLeft + dx;
+ let newTop = startTop + dy;
+
+ // Keep within viewport bounds
+ const rect = panel.getBoundingClientRect();
+ newLeft = Math.max(0, Math.min(window.innerWidth - rect.width, newLeft));
+ newTop = Math.max(0, Math.min(window.innerHeight - rect.height, newTop));
+
+ panel.style.left = newLeft + 'px';
+ panel.style.top = newTop + 'px';
+ };
+
+ const onMouseUp = () => {
+ isDragging = false;
+ document.removeEventListener('mousemove', onMouseMove);
+ document.removeEventListener('mouseup', onMouseUp);
+ };
+
+ // Remove existing listeners before adding new ones
+ header.removeEventListener('mousedown', header._dragHandler);
+ header._dragHandler = onMouseDown;
+ header.addEventListener('mousedown', onMouseDown);
+ }
+
renderTaskPanel() {
const session = this.sessions.get(this.activeSessionId);
const body = document.getElementById('backgroundTasksBody');
@@ -1561,32 +1647,103 @@ class ClaudemanApp {
}
}
- async killAllSessions() {
+ killAllSessions() {
const count = this.screenSessions?.length || 0;
if (count === 0) {
alert('No sessions to kill');
return;
}
- const confirmed = confirm(`Kill all ${count} session(s)? This cannot be undone.`);
- if (!confirmed) return;
+ // Show the kill all modal
+ document.getElementById('killAllCount').textContent = count;
+ document.getElementById('killAllModal').classList.add('active');
+ }
+
+ closeKillAllModal() {
+ document.getElementById('killAllModal').classList.remove('active');
+ }
+
+ async confirmKillAll(killScreens) {
+ this.closeKillAllModal();
try {
- const res = await fetch('/api/sessions', { method: 'DELETE' });
- const data = await res.json();
- if (data.success) {
- // Clear local state
+ if (killScreens) {
+ // Kill everything including screens
+ const res = await fetch('/api/sessions', { method: 'DELETE' });
+ const data = await res.json();
+ if (data.success) {
+ this.sessions.clear();
+ this.screenSessions = [];
+ this.activeSessionId = null;
+ this.renderSessionTabs();
+ this.renderScreenSessions();
+ this.terminal.clear();
+ this.terminal.reset();
+ this.toast('All sessions and screens killed', 'success');
+ }
+ } else {
+ // Just remove tabs, keep screens running
this.sessions.clear();
- this.screenSessions = [];
this.activeSessionId = null;
this.renderSessionTabs();
- this.renderScreenSessions();
this.terminal.clear();
this.terminal.reset();
+ this.toast('All tabs removed, screens still running', 'info');
}
} catch (err) {
- console.error('Failed to kill all sessions:', err);
- alert('Failed to kill sessions: ' + err.message);
+ console.error('Failed to kill sessions:', err);
+ this.toast('Failed to kill sessions: ' + err.message, 'error');
+ }
+ }
+
+ // ========== Create Case Modal ==========
+
+ showCreateCaseModal() {
+ document.getElementById('newCaseName').value = '';
+ document.getElementById('newCaseDescription').value = '';
+ document.getElementById('createCaseModal').classList.add('active');
+ document.getElementById('newCaseName').focus();
+ }
+
+ closeCreateCaseModal() {
+ document.getElementById('createCaseModal').classList.remove('active');
+ }
+
+ async createCase() {
+ const name = document.getElementById('newCaseName').value.trim();
+ const description = document.getElementById('newCaseDescription').value.trim();
+
+ if (!name) {
+ this.toast('Please enter a case name', 'error');
+ return;
+ }
+
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
+ this.toast('Invalid name. Use only letters, numbers, hyphens, underscores.', 'error');
+ return;
+ }
+
+ try {
+ const res = await fetch('/api/cases', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, description })
+ });
+
+ const data = await res.json();
+ if (data.success) {
+ this.closeCreateCaseModal();
+ this.toast(`Case "${name}" created`, 'success');
+ // Reload cases and select the new one
+ await this.loadQuickStartCases();
+ document.getElementById('quickStartCase').value = name;
+ this.updateDirDisplayForCase(name);
+ } else {
+ this.toast(data.error || 'Failed to create case', 'error');
+ }
+ } catch (err) {
+ console.error('Failed to create case:', err);
+ this.toast('Failed to create case: ' + err.message, 'error');
}
}
@@ -1701,26 +1858,38 @@ class ClaudemanApp {
updateSystemStatsDisplay(stats) {
const cpuEl = document.getElementById('statCpu');
+ const cpuBar = document.getElementById('statCpuBar');
const memEl = document.getElementById('statMem');
+ const memBar = document.getElementById('statMemBar');
- if (cpuEl) {
- cpuEl.textContent = `CPU: ${stats.cpu}%`;
- // Color based on usage
+ if (cpuEl && cpuBar) {
+ cpuEl.textContent = `${stats.cpu}%`;
+ cpuBar.style.width = `${Math.min(100, stats.cpu)}%`;
+
+ // Color classes based on usage
+ cpuBar.classList.remove('medium', 'high');
+ cpuEl.classList.remove('high');
if (stats.cpu > 80) {
+ cpuBar.classList.add('high');
cpuEl.classList.add('high');
- } else {
- cpuEl.classList.remove('high');
+ } else if (stats.cpu > 50) {
+ cpuBar.classList.add('medium');
}
}
- if (memEl) {
+ if (memEl && memBar) {
const memGB = (stats.memory.usedMB / 1024).toFixed(1);
- memEl.textContent = `Mem: ${memGB}GB`;
- // Color based on usage
+ memEl.textContent = `${memGB}G`;
+ memBar.style.width = `${Math.min(100, stats.memory.percent)}%`;
+
+ // Color classes based on usage
+ memBar.classList.remove('medium', 'high');
+ memEl.classList.remove('high');
if (stats.memory.percent > 80) {
+ memBar.classList.add('high');
memEl.classList.add('high');
- } else {
- memEl.classList.remove('high');
+ } else if (stats.memory.percent > 50) {
+ memBar.classList.add('medium');
}
}
}
diff --git a/src/web/public/index.html b/src/web/public/index.html
index a76c7259..b7582b4c 100644
--- a/src/web/public/index.html
+++ b/src/web/public/index.html
@@ -36,8 +36,20 @@
@@ -74,7 +86,7 @@