diff --git a/.gitignore b/.gitignore index f937b338..603da534 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ node_modules/ # Build output dist/ +# Vendored frontend deps (generated from node_modules by postinstall/build) +src/web/public/vendor/ + # Test coverage coverage/ diff --git a/CLAUDE.md b/CLAUDE.md index db422e34..b0c9e8c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ When user says "COM": 1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version CLAUDE.md`) 2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web` -**Version**: 0.1536 (must match `package.json` for npm publish) +**Version**: 0.1537 (must match `package.json` for npm publish) ## Project Overview diff --git a/docs/first-load-optimization-plan.md b/docs/first-load-optimization-plan.md new file mode 100644 index 00000000..437483b9 --- /dev/null +++ b/docs/first-load-optimization-plan.md @@ -0,0 +1,409 @@ +# First-Load Performance Optimization Plan + +**Date**: 2026-02-18 +**Audit by**: 4-agent team (css-analyst, js-analyst, server-analyst, deps-analyst) +**Scope**: First browser load of Claudeman web UI at `/` + +--- + +## Current State (Baseline) + +### Payload + +| Asset | Raw | Compressed | Render-Blocking? | +|-------|-----|-----------|-----------------| +| `index.html` | 82 KB | ~15 KB | N/A (document) | +| `styles.css` | 154 KB | ~25 KB | **YES** | +| `mobile.css` | 34 KB | ~7 KB | **YES** (missing media query) | +| `xterm.css` (CDN) | 2 KB | ~2 KB | No (preload pattern) | +| `xterm.min.js` (CDN) | 67 KB | ~65 KB | No (defer) | +| `xterm-addon-fit` (CDN) | 1 KB | ~1 KB | No (defer) | +| `app.js` | 563 KB | ~126 KB | No (defer) | +| **Total** | **903 KB** | **~241 KB** | | + +### Request Waterfall (13 requests on first load) + +``` +T=0 GET / (82KB doc) +T+20ms ├── styles.css?v=0.1536 (154KB — BLOCKS RENDER) + ├── mobile.css?v=0.1536 (34KB — BLOCKS RENDER on all viewports!) + ├── xterm.css (CDN, preloaded) (2KB — non-blocking, already async) + ├── xterm.min.js (CDN, defer) (67KB) + ├── xterm-addon-fit.min.js (CDN) (1KB) + └── app.js?v=0.1536 (defer) (563KB) + +[FIRST PAINT blocked by: styles.css + mobile.css] + +T+200ms JS execution starts + ├── new Terminal() + terminal.open() ← HEAVY sync (canvas creation) + ├── connectSSE() → /api/events ← SSE stream + ├── loadState() → /api/status ← DUPLICATE of SSE init! + ├── loadQuickStartCases() + │ ├── /api/settings ← fetched TWICE + │ └── /api/cases?_t= ← cache-busted unnecessarily + ├── startSystemStatsPolling() + │ └── /api/system/stats ← starts immediately, every 2s + └── loadAppSettingsFromServer() + └── /api/settings ← DUPLICATE #2 + +T+500ms First Meaningful Paint (terminal + header visible) +``` + +### Problems + +1. **2 render-blocking CSS files** — mobile.css blocks desktop for no reason +2. **Double handleInit()** — SSE init + /api/status both call full state reset +3. **Duplicate /api/settings** — fetched twice in init chain +4. **563KB unminified JS monolith** — no build minification at all +5. **154KB unminified CSS** — 70% is for modals/wizards (below-the-fold) +6. **Sync terminal.open()** — heaviest single call, blocks before first paint +7. **12 modals pre-rendered** — ~600+ hidden DOM nodes, ~60KB HTML +8. **Stats polling starts immediately** — even with 0 sessions +9. **No loading skeleton** — blank black screen until all CSS+JS loads +10. **CDN dependency** — 3 xterm files from jsdelivr (DNS+TLS latency) +11. **1h cache for versioned assets** — could be 1yr+immutable with ?v= busting +12. **No HTTP/2** — 6-connection limit queues some requests +13. **On-the-fly compression** — no pre-compressed .gz/.br files + +### What's Already Good (don't touch) + +- Single shared Terminal instance (buffer swapping) +- Teammate terminals created lazily on window open +- Subagent windows use HTML logs, not Terminal instances +- `getLightState()` has 1s TTL cache +- SSE init sends lightweight state (no terminal buffers) +- Buffer hydration uses chunked writes (128KB via rAF) +- `selectSession()` defers secondary panels via requestIdleCallback +- System fonts only — zero web font loading +- xterm.css already uses async preload pattern +- Proper SSE reconnection with exponential backoff +- CSS `contain` on header/tabs for layout isolation + +--- + +## Implementation Plan (15 steps, ordered by impact/effort) + +### Phase 1: Quick Wins (1-line to 15-min changes) + +#### Step 1: Add media attribute to mobile.css +**Impact**: HIGH — 34KB stops blocking render on desktop +**File**: `src/web/public/index.html:14` + +```html + + + + + +``` + +Browser still downloads it (for potential resize) but won't block rendering on desktop. The mobile.css file header says this was intended but never implemented. + +--- + +#### Step 2: Remove duplicate /api/status + double handleInit() +**Impact**: HIGH — eliminates redundant API call + double state reset (clears 15+ Maps, 7+ timers, runs cleanupAllFloatingWindows(), double renderSessionTabs()) +**Files**: `src/web/public/app.js` + +The SSE `init` event (server.ts:618) sends `getLightState()`. The `loadState()` in `init()` at `app.js:1554` fetches identical data from `/api/status`. Both call `handleInit()` which wipes state. The `_initGeneration` guard only protects session-restore, NOT the expensive cleanup (lines 3389-3503). + +```js +// In init() — REMOVE this.loadState(), add SSE fallback: +this.connectSSE(); +// Remove: this.loadState(); +this._initFallbackTimer = setTimeout(() => { + if (this._initGeneration === 0) this.loadState(); +}, 3000); + +// In handleInit() — clear fallback timer: +handleInit(data) { + if (this._initFallbackTimer) { + clearTimeout(this._initFallbackTimer); + this._initFallbackTimer = null; + } + // ... rest of handleInit +} +``` + +--- + +#### Step 3: Deduplicate /api/settings fetch +**Impact**: MEDIUM — removes 1 redundant API call +**Files**: `src/web/public/app.js:7341` (loadQuickStartCases), `app.js:9964` (loadAppSettingsFromServer) + +```js +// In init() — fetch settings once, share the promise: +const settingsPromise = fetch('/api/settings').then(r => r.json()); +this.loadQuickStartCases(null, settingsPromise); +this.loadAppSettingsFromServer(settingsPromise); +``` + +Both functions need to accept an optional pre-fetched settings promise parameter. + +--- + +#### Step 4: Remove cache-busting from /api/cases +**Impact**: LOW — allows browser caching +**File**: `src/web/public/app.js:7351` + +```js +// BEFORE +const res = await fetch('/api/cases?_t=' + Date.now()); +// AFTER +const res = await fetch('/api/cases'); +``` + +--- + +#### Step 5: Defer system stats polling +**Impact**: MEDIUM — removes API call every 2s when idle +**Files**: `src/web/public/app.js:1567`, `app.js:15261` + +Move `startSystemStatsPolling()` out of `init()`. Start it in `handleInit()` only when `data.sessions.length > 0`. + +--- + +### Phase 2: Build Pipeline (30-min changes, highest payload impact) + +#### Step 6: Self-host xterm.js assets +**Impact**: MEDIUM-HIGH — eliminates CDN DNS/TLS latency (~100ms even with preconnect) +**Files**: `src/web/public/index.html`, `package.json` build script + +xterm is NOT in package.json — add it: +```bash +npm install xterm@5.3.0 @xterm/addon-fit@0.8.0 --save +``` + +Build script addition: +```bash +mkdir -p dist/web/public/vendor +cp node_modules/xterm/css/xterm.css dist/web/public/vendor/ +cp node_modules/xterm/lib/xterm.min.js dist/web/public/vendor/ +cp node_modules/@xterm/addon-fit/lib/xterm-addon-fit.min.js dist/web/public/vendor/ +``` + +Update index.html CDN URLs to `/vendor/xterm.min.js` etc. Remove preconnect/dns-prefetch for jsdelivr. + +--- + +#### Step 7: Add esbuild minification to build +**Impact**: HIGH — biggest single optimization for payload size +**File**: `package.json` build script + +Current build just does `cp -r src/web/public dist/web/`. No minification. + +app.js stats: 1,525 comment lines (10%), 89 console.* statements, 23% whitespace. + +```bash +# Add to build script after cp: +npx esbuild dist/web/public/app.js --minify --drop:console --outfile=dist/web/public/app.js --allow-overwrite +npx esbuild dist/web/public/styles.css --minify --outfile=dist/web/public/styles.css --allow-overwrite +npx esbuild dist/web/public/mobile.css --minify --outfile=dist/web/public/mobile.css --allow-overwrite +``` + +Expected savings: +| File | Before (gzip) | After (gzip) | Saved | +|------|---------------|-------------|-------| +| app.js | ~126 KB | ~85 KB | ~41 KB (33%) | +| styles.css | ~25 KB | ~18 KB | ~7 KB (28%) | +| mobile.css | ~7 KB | ~5 KB | ~2 KB (29%) | +| **Total** | **~158 KB** | **~108 KB** | **~50 KB** | + +--- + +#### Step 8: Pre-compress static assets at build time +**Impact**: MEDIUM — eliminates per-request CPU compression +**Files**: `package.json` build script, potentially `src/web/server.ts` + +```bash +# Add to build script after minification: +for f in dist/web/public/*.{js,css,html}; do + gzip -9 -k "$f" + brotli -9 -k "$f" +done +``` + +Check if `@fastify/static` supports `preCompressed: true` option. If not, serve pre-compressed files via custom Accept-Encoding check. + +--- + +#### Step 9: Extend cache duration for versioned assets +**Impact**: LOW (first load) / HIGH (repeat visits) +**File**: `src/web/server.ts:601` + +```js +// BEFORE +maxAge: '1h' + +// AFTER +maxAge: '1y', +immutable: true +``` + +Safe because all assets use `?v=0.1536` cache-busting. First-load unaffected, but all repeat visits serve from disk cache instantly. + +--- + +### Phase 3: Perceived Performance (30-60min, user experience) + +#### Step 10: Add loading skeleton +**Impact**: MEDIUM-HIGH — instant visual structure instead of black screen +**File**: `src/web/public/index.html` + +Add minimal inline ` +
+
+
+
+``` + +In `app.js` init() end: `document.body.classList.add('app-loaded');` + +--- + +#### Step 11: Defer terminal creation to after first paint +**Impact**: MEDIUM-HIGH — terminal.open() is heaviest sync call +**File**: `src/web/public/app.js:1545` + +```js +init() { + // ... mobile detection, visibility settings ... + document.documentElement.classList.remove('mobile-init'); + + // Show skeleton immediately, defer heavy terminal init + requestAnimationFrame(() => { + this.initTerminal(); + this.connectSSE(); + // ... rest of init + }); +} +``` + +Lets browser paint header/tabs/skeleton before canvas creation. + +--- + +### Phase 4: DOM + Payload Reduction (1-3 hours) + +#### Step 12: Lazy-create modals on first open +**Impact**: HIGH — removes ~600+ DOM nodes, ~60KB hidden HTML +**Files**: `src/web/public/index.html`, `src/web/public/app.js` + +12 modals pre-rendered in index.html: +- `helpModal` (lines 227-447) +- `sessionOptionsModal` (lines 448-714) — 266 lines +- `appSettingsModal` (lines 715-900+) +- `createCaseModal`, `mobileCasePickerModal`, `ralphWizardModal`, `killAllModal`, `closeConfirmModal`, `savePresetModal`, `tokenStatsModal`, `filePreviewModal`, notification drawer + +Replace each modal's HTML with ``. On first open, inject full HTML via template function. Cache after creation. + +--- + +#### Step 13: Batch initial API calls into one endpoint +**Impact**: MEDIUM — reduces 4+ API calls to 1 +**Files**: `src/web/server.ts`, `src/web/public/app.js` + +Create `GET /api/init-bundle`: +```json +{ + "status": { /* getLightState() */ }, + "cases": [ /* case list */ ], + "settings": { /* user settings */ } +} +``` + +Use as SSE init fallback (step 2's timeout). Saves HTTP round trips. + +--- + +#### Step 14: Trim SSE init payload +**Impact**: LOW-MEDIUM — reduces init payload by removing data not needed for first paint +**File**: `src/web/server.ts` + +Remove from SSE init event: `taskTree`, `ralphTodos`, `ralphTodoStats` per session. These can be fetched on-demand when user opens a session's details panel. + +--- + +#### Step 15: Enable HTTP/2 +**Impact**: MEDIUM — multiplexed loading over single connection +**File**: `src/web/server.ts` + +```js +// BEFORE +const server = Fastify({ logger: false }); + +// AFTER (when HTTPS is enabled) +const server = Fastify({ + logger: false, + http2: true // Only works with HTTPS +}); +``` + +Only applicable for `--https` mode. HTTP/2 multiplexing eliminates the 6-connection limit queuing. + +--- + +## Expected Combined Impact + +| Metric | Before | After | Improvement | +|--------|--------|-------|-------------| +| First Paint | ~300ms | ~100ms | **-200ms** (skeleton visible instantly) | +| First Contentful Paint | ~400ms | ~200ms | **-200ms** (no mobile.css blocking desktop) | +| Time to Interactive | ~600ms | ~350ms | **-250ms** (fewer API calls, deferred terminal) | +| Total compressed payload | ~241 KB | ~191 KB | **-50 KB (21%)** via minification | +| Init API calls | 6-7 (2 dupes) | 2-3 | **-60%** fewer requests | +| Initial DOM nodes | ~1800+ | ~1200 | **-600** (lazy modals) | + +--- + +## Verification + +After each step, verify with Playwright: + +```js +const { chromium } = require('playwright'); +const browser = await chromium.launch(); +const page = await browser.newPage(); + +// Measure first paint +await page.goto('http://localhost:3000', { waitUntil: 'domcontentloaded' }); +await page.waitForTimeout(4000); // Wait for async data + +// Check UI renders correctly +const header = await page.locator('.header').isVisible(); +const tabs = await page.locator('.session-tabs').isVisible(); +const terminal = await page.locator('.terminal-container').isVisible(); +console.log({ header, tabs, terminal }); + +await browser.close(); +``` + +--- + +## Files Changed Per Step (for implementation agent) + +| Step | Files Modified | +|------|---------------| +| 1 | `index.html` | +| 2 | `app.js` | +| 3 | `app.js` | +| 4 | `app.js` | +| 5 | `app.js` | +| 6 | `index.html`, `package.json` | +| 7 | `package.json` | +| 8 | `package.json`, optionally `server.ts` | +| 9 | `server.ts` | +| 10 | `index.html`, `app.js` | +| 11 | `app.js` | +| 12 | `index.html`, `app.js` | +| 13 | `server.ts`, `app.js` | +| 14 | `server.ts` | +| 15 | `server.ts` | diff --git a/package-lock.json b/package-lock.json index 2d5aee25..358fd103 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claudeman", - "version": "0.1503", + "version": "0.1536", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claudeman", - "version": "0.1503", + "version": "0.1536", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -18,6 +18,8 @@ "fastify": "^5.1.0", "node-pty": "^1.1.0", "uuid": "^10.0.0", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0", "zod": "^4.3.6" }, "bin": { @@ -29,6 +31,7 @@ "@types/uuid": "^10.0.0", "@vitest/coverage-v8": "^4.0.18", "agent-browser": "^0.6.0", + "esbuild": "^0.27.3", "pixelmatch": "^6.0.0", "playwright": "^1.58.0", "pngjs": "^7.0.0", @@ -124,9 +127,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -141,9 +144,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -158,9 +161,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -175,9 +178,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -192,9 +195,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -209,9 +212,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -226,9 +229,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -243,9 +246,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -260,9 +263,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -277,9 +280,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -294,9 +297,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -311,9 +314,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -328,9 +331,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -345,9 +348,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -362,9 +365,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -379,9 +382,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -396,9 +399,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -413,9 +416,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -430,9 +433,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -447,9 +450,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -464,9 +467,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -481,9 +484,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -498,9 +501,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -515,9 +518,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -532,9 +535,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -549,9 +552,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -2302,9 +2305,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2315,32 +2318,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/escalade": { @@ -4842,6 +4845,23 @@ "node": ">=0.4" } }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", + "license": "MIT" + }, + "node_modules/xterm-addon-fit": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz", + "integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==", + "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.", + "license": "MIT", + "peerDependencies": { + "xterm": "^5.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 8b782db6..22a0f0b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1536", + "version": "0.1537", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", @@ -10,7 +10,7 @@ }, "scripts": { "postinstall": "node scripts/postinstall.js", - "build": "tsc && chmod +x dist/index.js && mkdir -p dist/web dist/templates && cp -r src/web/public dist/web/ && cp src/templates/case-template.md dist/templates/", + "build": "tsc && chmod +x dist/index.js && mkdir -p dist/web dist/templates dist/web/public/vendor && cp -r src/web/public dist/web/ && cp src/templates/case-template.md dist/templates/ && cp node_modules/xterm/css/xterm.css dist/web/public/vendor/ && npx esbuild node_modules/xterm/lib/xterm.js --minify --outfile=dist/web/public/vendor/xterm.min.js && npx esbuild node_modules/xterm-addon-fit/lib/xterm-addon-fit.js --minify --outfile=dist/web/public/vendor/xterm-addon-fit.min.js && npx esbuild dist/web/public/app.js --minify --drop:console --outfile=dist/web/public/app.js --allow-overwrite && npx esbuild dist/web/public/styles.css --minify --outfile=dist/web/public/styles.css --allow-overwrite && npx esbuild dist/web/public/mobile.css --minify --outfile=dist/web/public/mobile.css --allow-overwrite", "start": "node dist/index.js", "dev": "tsx src/index.ts", "web": "node dist/index.js web", @@ -45,6 +45,8 @@ "fastify": "^5.1.0", "node-pty": "^1.1.0", "uuid": "^10.0.0", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0", "zod": "^4.3.6" }, "devDependencies": { @@ -53,6 +55,7 @@ "@types/uuid": "^10.0.0", "@vitest/coverage-v8": "^4.0.18", "agent-browser": "^0.6.0", + "esbuild": "^0.27.3", "pixelmatch": "^6.0.0", "playwright": "^1.58.0", "pngjs": "^7.0.0", diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 911c9027..26c9f170 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -206,6 +206,38 @@ if (claudeFound) { console.log(colors.cyan(' curl -fsSL https://claude.ai/install.sh | bash')); } +// ---------------------------------------------------------------------------- +// 4. Copy xterm vendor files for dev mode (src/web/public/vendor/) +// ---------------------------------------------------------------------------- + +try { + const require = createRequire(import.meta.url); + const xtermDir = join(require.resolve('xterm'), '..', '..'); + const fitDir = join(require.resolve('xterm-addon-fit'), '..', '..'); + const vendorDir = join(import.meta.dirname, '..', 'src', 'web', 'public', 'vendor'); + + const { mkdirSync, copyFileSync } = await import('fs'); + mkdirSync(vendorDir, { recursive: true }); + copyFileSync(join(xtermDir, 'css', 'xterm.css'), join(vendorDir, 'xterm.css')); + + // Minify xterm JS for dev vendor dir (npm packages don't ship .min.js) + try { + execSync(`npx esbuild "${join(xtermDir, 'lib', 'xterm.js')}" --minify --outfile="${join(vendorDir, 'xterm.min.js')}"`, { stdio: 'pipe' }); + execSync(`npx esbuild "${join(fitDir, 'lib', 'xterm-addon-fit.js')}" --minify --outfile="${join(vendorDir, 'xterm-addon-fit.min.js')}"`, { stdio: 'pipe' }); + console.log(colors.green('✓ xterm vendor files copied to src/web/public/vendor/')); + } catch { + // Fallback: copy unminified + copyFileSync(join(xtermDir, 'lib', 'xterm.js'), join(vendorDir, 'xterm.min.js')); + copyFileSync(join(fitDir, 'lib', 'xterm-addon-fit.js'), join(vendorDir, 'xterm-addon-fit.min.js')); + console.log(colors.green('✓ xterm vendor files copied') + colors.dim(' (unminified — esbuild not available)')); + } +} catch (err) { + hasWarnings = true; + console.log(colors.yellow('⚠ Failed to copy xterm vendor files')); + console.log(colors.dim(` ${err.message}`)); + console.log(colors.dim(' Dev server may fail to load xterm.js — run: npm run build')); +} + // ---------------------------------------------------------------------------- // Print Summary and Next Steps // ---------------------------------------------------------------------------- diff --git a/src/web/public/app.js b/src/web/public/app.js index 93f15361..e363612f 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -1543,14 +1543,17 @@ class ClaudemanApp { KeyboardHandler.init(); SwipeHandler.init(); KeyboardAccessoryBar.init(); - this.initTerminal(); - this.loadFontSize(); this.applyHeaderVisibilitySettings(); this.applyTabWrapSettings(); this.applyMonitorVisibility(); // Remove mobile-init class now that JS has applied visibility settings. // The inline - + + + + + +
+
+ Claudeman +
+
+
+
+
diff --git a/src/web/server.ts b/src/web/server.ts index 71e421c7..a552fac1 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -593,12 +593,12 @@ export class WebServer extends EventEmitter { done(); }); - // Serve static files with caching headers for immutable CDN-like assets + // Serve static files — versioned assets (?v=X) are immutable, cache aggressively await this.app.register(fastifyStatic, { root: join(__dirname, 'public'), prefix: '/', - // Cache static assets for 1 hour (they change on deploy, not during session) - maxAge: '1h', + maxAge: '1y', + immutable: true, }); // SSE endpoint for real-time updates