mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-14 12:46:05 +00:00
fix: handle HQ mode shutdown gracefully to prevent e2e test errors
- Add global shutdown state tracking via shutdown-state.ts module - Update refresh-sessions endpoint to return 503 during shutdown - Skip HQ notifications in control-dir-watcher during shutdown - Disable remote health checks during server shutdown - Suppress expected connection errors when servers are shutting down This prevents the flood of "Failed to refresh sessions" and "Failed to notify HQ" errors that were appearing in the HQ e2e test logs when servers were shutting down. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f86d089226
commit
b1718c27fa
7 changed files with 107 additions and 177 deletions
|
|
@ -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!
|
||||
206
web/README.md
206
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 <command> [args...]
|
||||
|
||||
# Monitor-only mode (no input)
|
||||
npx tsx src/fwd.ts --monitor-only <command>
|
||||
```
|
||||
|
||||
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=<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
|
||||
- **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
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
// 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)
|
||||
|
|
|
|||
15
web/src/server/services/shutdown-state.ts
Normal file
15
web/src/server/services/shutdown-state.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in a new issue