diff --git a/web/CLAUDE.md b/web/CLAUDE.md index 0f0e66ae..64f04566 100644 --- a/web/CLAUDE.md +++ b/web/CLAUDE.md @@ -1,7 +1,6 @@ # Claude Development Notes -After receiving the first user mesage, read spec.md before you proceed. The spec.md contains a map of this code base that should help you navigate it. - +**IMPORTANT**: BEFORE YOU DO ANYTHING, READ spec.md IN FULL USING THE READ TOOL! **IMPORTANT**: NEVER USE GREP. ALWAYS USE RIPGREP! ## Updating spec.md @@ -37,6 +36,7 @@ As code changes, the spec.md might get outdated. If you detect outdated informat - `npm run lint:fix` - `npm run typecheck` - Always fix all linting and type checking errors. +- Never run the tests, unless explicitely asked to. `npm run test` ## Server Execution - NEVER RUN THE SERVER YOURSELF, I ALWAYS RUN IT ON THE SIDE VIA NPM RUN DEV! \ No newline at end of file diff --git a/web/README.md b/web/README.md index 5abfc621..5ede32c7 100644 --- a/web/README.md +++ b/web/README.md @@ -1,179 +1,55 @@ -# VibeTunnel Web Frontend +# VibeTunnel -A modern web interface for the VibeTunnel terminal multiplexer built with TypeScript, Lit Elements, and XTerm.js. Provides professional terminal emulation with mobile-optimized controls and real-time session management. - -## Features - -- **Professional Terminal Emulation** using XTerm.js with full VT compatibility -- **Real-time Session Management** with live streaming via Server-Sent Events -- **Mobile-Optimized Interface** with touch controls and responsive design -- **Session Snapshots** for previewing terminal output in card view -- **Interactive Terminal Input** with full keyboard support and mobile input overlay -- **VS Code Dark Theme** with consistent styling throughout -- **Custom Font Support** using Fira Code with programming ligatures -- **File Browser** for selecting working directories -- **Session Lifecycle Management** (create, monitor, kill, cleanup) +Web-based terminal multiplexer with distributed architecture support. ## Quick Start -1. **Install dependencies:** - ```bash - npm install - ``` +```bash +npm install +npm run dev # Starts server + auto-rebuilds +``` -2. **Start development server:** - ```bash - npm run dev - ``` +Open http://localhost:3000 -3. **Open browser:** - Navigate to `http://localhost:3000` - -## Development Scripts +## Server Modes ```bash -# Development (auto-rebuild and watch) -npm run dev # Start full dev environment -npm run watch:server # Watch server TypeScript only -npm run watch:css # Watch CSS changes only +# Standalone server +npm run dev -# Building -npm run build # Build everything for production -npm run build:server # Build server TypeScript -npm run build:client # Build client TypeScript -npm run build:css # Build Tailwind CSS +# HQ server (manages remote servers) +npm run dev -- --hq -# Bundling (ES modules) -npm run bundle # Bundle client code -npm run bundle:watch # Watch and bundle client code - -# Code Quality -npm run lint # Check ESLint issues -npm run lint:fix # Auto-fix ESLint issues -npm run format # Format code with Prettier -npm run format:check # Check code formatting -npm run pre-commit # Run all quality checks - -# Testing -npm run test # Run Jest tests -npm run test:watch # Watch and run tests +# Remote server (connects to HQ) +npm run dev -- --hq-url http://hq-server:3000 --name remote1 ``` +## Build & Test + +```bash +npm run build # Production build +npm run lint # Check code style +npm run typecheck # Type checking +npm test # Run tests +``` + +## fwd Tool + +CLI that spawns PTY sessions integrated with VibeTunnel: + +```bash +# Forward a command to VibeTunnel +npx tsx src/fwd.ts [args...] + +# Monitor-only mode (no input) +npx tsx src/fwd.ts --monitor-only +``` + +Creates persistent sessions accessible via the web UI. + ## Architecture -### Client-Side Components - -Built with **Lit Elements** (Web Components): - -``` -src/client/ -├── app.ts # Main application controller -├── components/ -│ ├── app-header.ts # Main navigation and controls -│ ├── session-list.ts # Session grid with cards -│ ├── session-card.ts # Individual session preview -│ ├── session-view.ts # Full terminal view -│ ├── session-create-form.ts # New session modal -│ └── file-browser.ts # Directory selection -├── renderer.ts # XTerm.js terminal renderer -└── scale-fit-addon.ts # Custom terminal scaling -``` - -### Server-Side Architecture - -**Express.js** server with **tty-fwd integration**: - -``` -src/ -├── server.ts # Main Express server -└── input.css # Tailwind source styles -``` - -### Build Output - -``` -dist/ # Compiled TypeScript -public/ -├── bundle/ -│ ├── client-bundle.js # Bundled client code -│ ├── renderer.js # Terminal renderer -│ └── output.css # Compiled styles -├── fonts/ # Fira Code font files -└── index.html # Main HTML -``` - -## API Reference - -### Session Management - -``` -GET /api/sessions # List all sessions -POST /api/sessions # Create new session -DELETE /api/sessions/:id # Kill session -DELETE /api/sessions/:id/cleanup # Clean up session files -POST /api/cleanup-exited # Clean all exited sessions -``` - -### Terminal I/O - -``` -GET /api/sessions/:id/stream # Live session stream (SSE) -GET /api/sessions/:id/snapshot # Session snapshot (cast format) -POST /api/sessions/:id/input # Send input to session -``` - -### File System - -``` -GET /api/fs/browse?path= # Browse directories -POST /api/mkdir # Create directory -``` - -## Technology Stack - -- **Frontend Framework:** Lit Elements (Web Components) -- **Terminal Emulation:** XTerm.js with custom addons -- **Styling:** Tailwind CSS with VS Code theme -- **Typography:** Fira Code Variable Font -- **Backend:** Express.js + TypeScript -- **Terminal Backend:** tty-fwd (Rust binary) -- **Build Tools:** TypeScript, ESBuild, Tailwind -- **Code Quality:** ESLint, Prettier, Pre-commit hooks - -## Mobile Support - -- **Touch-optimized scrolling** with proper overscroll prevention -- **Mobile input overlay** with virtual keyboard support -- **Responsive design** with mobile-first approach -- **Gesture navigation** (swipe from edge to go back) -- **Pull-to-refresh prevention** during terminal interaction - -## Browser Compatibility - -- **Modern ES6+ browsers** (Chrome 63+, Firefox 67+, Safari 13+) -- **Mobile browsers** with full touch support -- **Progressive enhancement** with graceful degradation - -## Deployment - -1. **Build for production:** - ```bash - npm run build - ``` - -2. **Start production server:** - ```bash - npm start - ``` - -3. **Environment variables:** - - `PORT` - Server port (default: 3000) - - `TTY_FWD_CONTROL_DIR` - tty-fwd control directory - -## Development Notes - -- **Hot reload** enabled in development -- **TypeScript strict mode** with comprehensive type checking -- **ESLint + Prettier** enforced via pre-commit hooks -- **Component-based architecture** for maintainability -- **Mobile-first responsive design** principles \ No newline at end of file +- **Server**: Express + node-pty for terminal sessions +- **Client**: Lit web components + xterm.js +- **Streaming**: SSE for output, WebSocket for binary buffers +- **Protocol**: Binary-optimized terminal state synchronization \ No newline at end of file diff --git a/web/src/server/index.ts b/web/src/server/index.ts index 85163891..30b7ccba 100644 --- a/web/src/server/index.ts +++ b/web/src/server/index.ts @@ -1,5 +1,6 @@ import chalk from 'chalk'; import { createApp } from './app.js'; +import { setShuttingDown } from './services/shutdown-state.js'; // Create and configure the app const appInstance = createApp(); @@ -31,6 +32,7 @@ if (isMainModule) { } isShuttingDown = true; + setShuttingDown(true); console.log(chalk.yellow('\nShutting down...')); try { diff --git a/web/src/server/routes/remotes.ts b/web/src/server/routes/remotes.ts index 7d67eb33..b609730d 100644 --- a/web/src/server/routes/remotes.ts +++ b/web/src/server/routes/remotes.ts @@ -1,6 +1,7 @@ import { Router } from 'express'; import { RemoteRegistry } from '../services/remote-registry.js'; import chalk from 'chalk'; +import { isShuttingDown } from '../services/shutdown-state.js'; interface RemoteRoutesConfig { remoteRegistry: RemoteRegistry | null; @@ -74,6 +75,11 @@ export function createRemoteRoutes(config: RemoteRoutesConfig): Router { return res.status(404).json({ error: 'Not running in HQ mode' }); } + // If server is shutting down, return service unavailable + if (isShuttingDown()) { + return res.status(503).json({ error: 'Server is shutting down' }); + } + const remoteName = req.params.remoteName; const { action, sessionId } = req.body; @@ -109,6 +115,14 @@ export function createRemoteRoutes(config: RemoteRoutesConfig): Router { throw new Error(`Failed to fetch sessions: ${response.status}`); } } catch (error) { + // During shutdown, connection failures are expected + if (isShuttingDown()) { + console.log( + chalk.yellow(`Remote ${remote.name} refresh failed during shutdown (expected)`) + ); + return res.status(503).json({ error: 'Server is shutting down' }); + } + console.error(chalk.red(`Failed to refresh sessions for remote ${remote.name}:`), error); res.status(500).json({ error: 'Failed to refresh sessions' }); } diff --git a/web/src/server/services/control-dir-watcher.ts b/web/src/server/services/control-dir-watcher.ts index 465232c8..5e26995d 100644 --- a/web/src/server/services/control-dir-watcher.ts +++ b/web/src/server/services/control-dir-watcher.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import chalk from 'chalk'; import { RemoteRegistry } from './remote-registry.js'; import { HQClient } from './hq-client.js'; +import { isShuttingDown } from './shutdown-state.js'; interface ControlDirWatcherConfig { controlDir: string; @@ -57,7 +58,7 @@ export class ControlDirWatcher { console.log(chalk.blue(`Detected new external session: ${sessionId}`)); // If we're a remote server registered with HQ, immediately notify HQ - if (this.config.hqClient) { + if (this.config.hqClient && !isShuttingDown()) { try { await this.notifyHQAboutSession(sessionId, 'created'); } catch (error) { @@ -73,14 +74,17 @@ export class ControlDirWatcher { console.log(chalk.yellow(`Detected removed external session: ${sessionId}`)); // If we're a remote server registered with HQ, immediately notify HQ - if (this.config.hqClient) { + if (this.config.hqClient && !isShuttingDown()) { try { await this.notifyHQAboutSession(sessionId, 'deleted'); } catch (error) { - console.error( - chalk.red(`Failed to notify HQ about deleted session ${sessionId}:`), - error - ); + // During shutdown, this is expected + if (!isShuttingDown()) { + console.error( + chalk.red(`Failed to notify HQ about deleted session ${sessionId}:`), + error + ); + } } } @@ -98,7 +102,7 @@ export class ControlDirWatcher { sessionId: string, action: 'created' | 'deleted' ): Promise { - if (!this.config.hqClient) return; + if (!this.config.hqClient || isShuttingDown()) return; const hqUrl = this.config.hqClient.getHQUrl(); const hqAuth = this.config.hqClient.getHQAuth(); @@ -120,6 +124,10 @@ export class ControlDirWatcher { }); if (!response.ok) { + // If we get a 503 during shutdown, that's expected + if (response.status === 503 && isShuttingDown()) { + return; + } throw new Error(`HQ responded with ${response.status}`); } diff --git a/web/src/server/services/remote-registry.ts b/web/src/server/services/remote-registry.ts index f2a04b5f..e3244f73 100644 --- a/web/src/server/services/remote-registry.ts +++ b/web/src/server/services/remote-registry.ts @@ -1,3 +1,5 @@ +import { isShuttingDown } from './shutdown-state.js'; + export interface RemoteServer { id: string; name: string; @@ -116,6 +118,11 @@ export class RemoteRegistry { } private async checkRemoteHealth(remote: RemoteServer): Promise { + // Skip health checks during shutdown + if (isShuttingDown()) { + return; + } + try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.HEALTH_CHECK_TIMEOUT); @@ -139,14 +146,22 @@ export class RemoteRegistry { throw new Error(`HTTP ${response.status}`); } } catch (error) { - console.log(`Remote failed health check: ${remote.name} (${remote.id}) - ${error}`); - // Remove the remote if it fails health check - this.unregister(remote.id); + // During shutdown, don't log errors or unregister remotes + if (!isShuttingDown()) { + console.log(`Remote failed health check: ${remote.name} (${remote.id}) - ${error}`); + // Remove the remote if it fails health check + this.unregister(remote.id); + } } } private startHealthChecker() { this.healthCheckInterval = setInterval(() => { + // Skip health checks during shutdown + if (isShuttingDown()) { + return; + } + // Check all remotes in parallel const healthChecks = Array.from(this.remotes.values()).map((remote) => this.checkRemoteHealth(remote) diff --git a/web/src/server/services/shutdown-state.ts b/web/src/server/services/shutdown-state.ts new file mode 100644 index 00000000..4f5adb7c --- /dev/null +++ b/web/src/server/services/shutdown-state.ts @@ -0,0 +1,15 @@ +/** + * Global shutdown state management for the server. + * This module tracks whether the server is currently shutting down + * to allow various components to handle shutdown gracefully. + */ + +let shuttingDown = false; + +export function isShuttingDown(): boolean { + return shuttingDown; +} + +export function setShuttingDown(value: boolean): void { + shuttingDown = value; +}