diff --git a/CLAUDE.md b/CLAUDE.md index 3adee21e..50741ccc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,112 @@ -You are working autonomously in a Codeman worktree. -Before doing ANYTHING else, re-read `TASK.md` in this directory -and resume from the phase in `status`. -Do not rely on conversation history. -Then invoke the codeman-task-runner skill. +# Codeman — Developer Guide + +## Project Overview +- **Name**: Codeman (npm: `aicodeman`) +- **Description**: Control plane for AI coding agents — real-time monitoring, multi-session dashboard, mobile-first UI +- **Tech Stack**: TypeScript backend (Fastify 5), vanilla JS/CSS frontend, tmux for session muxing +- **Repo**: Fork at `maupet/Codeman`, upstream at `SGudbrandsson/Codeman` +- **Version**: 0.6.6 + +## Commands + +```bash +npm run build # Build everything (TS + frontend assets) → dist/ +npm run dev # Dev mode with tsx (live reload) +npm start # Run built app: node dist/index.js +npm run web # Run web server: node dist/index.js web +npm test # Run tests (vitest) +npm run test:watch # Tests in watch mode +npm run typecheck # TypeScript type checking (tsc --noEmit) +npm run lint # ESLint on src/**/*.ts +npm run lint:fix # ESLint with auto-fix +npm run format:check # Prettier check +npm run format # Prettier write +``` + +### Running a dev instance + +Codeman production runs on port 3000 via systemd (`codeman.service`) — it hosts every Claude Code session on this server, **including this one**. Never restart production to test dev changes. + +The dev instance runs on port 3001 from the `codeman-dev` checkout: +```bash +node dist/index.js web --port 3001 # Dev instance — safe to restart +``` + +**Deploy to dev:** `npm run build && rsync -a --delete dist/ ~/.codeman/app/dist/` then restart the dev process (not systemd). +**Deploy to prod:** Only after testing on dev. `sudo systemctl restart codeman` picks up the new dist. + +## Architecture + +``` +src/ +├── index.ts # Entry point +├── cli.ts # CLI command definitions (commander) +├── web/ +│ ├── server.ts # Fastify web server +│ └── public/ # Frontend (vanilla JS/CSS, no framework) +│ ├── index.html # Main HTML +│ ├── styles.css # Desktop styles +│ ├── mobile.css # Mobile/tablet styles (<1024px) +│ ├── app.js # Main app logic +│ ├── keyboard-accessory.js # Bottom bar / accessory buttons +│ ├── mobile-handlers.js # Touch/mobile detection +│ └── ... +├── session-manager.ts # Claude/OpenCode session lifecycle +├── orchestrator.ts # Session health monitoring +├── respawn-controller.ts # Auto-respawn logic (Ralph loops) +├── mux-factory.ts # tmux backend abstraction +├── vault/ # Session persistence / token tracking +├── config/ # App configuration +├── integrations/ # External service connectors +└── utils/ # Shared utilities +``` + +## Key Patterns + +### Frontend +- **No framework** — vanilla JS with manual DOM manipulation +- **CSS breakpoints** in `mobile.css`: + - `<430px` — phone + - `430-768px` — tablet + - `768-1023px` — large tablet + - `>=1024px` — desktop (uses `styles.css` only) +- `mobile.css` only loads for `<1024px` screens; global styles outside `@media` blocks apply to all mobile/tablet sizes +- **Keyboard accessory bar** — bottom action bar, built programmatically in `keyboard-accessory.js` +- Build step minifies CSS and injects content hashes into `index.html` + +### Backend +- Fastify 5 web server serving static files + WebSocket for real-time updates +- tmux sessions managed via `mux-factory.ts` / `mux-interface.ts` +- CLI uses `commander` with subcommands (`web`, `session`, etc.) +- Default web port: `3000` (configurable via `--port`) + +## Testing +- **Framework**: Vitest +- **Run**: `npm test` (or `npm run test:watch` for dev) +- Test files co-located or in `__tests__/` directories + +## Git Workflow +- Fork: `origin` → `maupet/Codeman` (GitHub) +- Upstream: `upstream` → `SGudbrandsson/Codeman` (GitHub) +- Branch naming: `fix/`, `feat/` +- PRs go from fork branches → upstream `master` +- Use `gh` CLI for GitHub operations (authenticated via `gh auth`) + +### After pushing a PR +1. Merge the fix branch into local `master`: `git merge fix/ --no-edit` +2. Rebuild: `npm run build` +3. Test on dev (port 3001) first — never deploy untested changes to prod +4. Update production: `rsync -a --delete dist/ ~/.codeman/app/dist/` +5. CSS/JS changes take effect on browser hard-refresh +6. Backend changes (orchestrator, server, etc.) need: `sudo systemctl restart codeman` + - **Never** `kill` + `nohup` the production process — it's managed by systemd + - **Never** restart prod to test — use the dev instance on port 3001 + +## Gotchas +- The upstream default branch is `master`, not `main` +- Build output goes to `dist/` — always rebuild after source changes +- CSS changes require `npm run build` then browser hard-refresh +- For quick CSS hotfixes to production: copy `dist/web/public/.css` directly +- Orchestrator dispatch worktrees go to `~/.codeman/dispatch-worktrees/`, not `~/codeman-cases/` +- `sendInput()` on a Session calls `runPrompt()` which spawns a NEW process — for interactive sessions use `writeViaMux()` instead +- Local `master` may be ahead of `origin/master` with unmerged PR fixes — this is intentional diff --git a/docs/superpowers/plans/2026-04-27-pwa-upgrade.md b/docs/superpowers/plans/2026-04-27-pwa-upgrade.md new file mode 100644 index 00000000..bfa1410a --- /dev/null +++ b/docs/superpowers/plans/2026-04-27-pwa-upgrade.md @@ -0,0 +1,659 @@ +# PWA Upgrade Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make Codeman a fully installable PWA with offline app shell caching, a session list snapshot for flaky networks, iOS support, and a service worker update prompt. + +**Architecture:** Hand-written Cache API in `sw.js` (no Workbox). Precache the app shell on install, NetworkFirst for `/api/sessions`, cache-first for static assets, network-only for everything else. Icons generated at build time from source SVG using `sharp`. + +**Tech Stack:** Vanilla JS, Cache API, sharp (existing dependency), `scripts/build.mjs` + +**Spec:** `docs/superpowers/specs/2026-04-27-pwa-upgrade-design.md` + +--- + +## File Map + +| File | Action | Responsibility | +|------|--------|----------------| +| `src/web/public/icons/icon.svg` | Create | Source SVG for icon generation | +| `src/web/public/manifest.json` | Modify | Add icons, description, scope, id, lang | +| `src/web/public/index.html` | Modify | Add iOS meta tags + apple-touch-icon link | +| `src/web/public/sw.js` | Rewrite | Add precache, fetch handler, SKIP_WAITING, keep push handlers | +| `src/web/public/app.js` | Modify | SW update detection + toast, stale data indicator on session fetch | +| `scripts/build.mjs` | Modify | Add sharp-based icon generation step | + +--- + +### Task 1: Source SVG & Icon Build Step + +**Files:** +- Create: `src/web/public/icons/icon.svg` +- Modify: `scripts/build.mjs` + +- [ ] **Step 1: Create the source SVG** + +Create `src/web/public/icons/icon.svg` — the existing lightning bolt design extracted from the inline data URI in `index.html`: + +```svg + + + + + + + + + + +``` + +This is the same design as the inline favicon but scaled to 512×512 viewBox. The `rx="96"` gives the same proportional rounding as `rx="6"` on the 32×32 original. + +- [ ] **Step 2: Add icon generation to build script** + +In `scripts/build.mjs`, add this block after the "copy web assets" step (after line 34, before the vendor xterm section): + +```js +// 2b. Generate PWA icons from source SVG +{ + const sharp = (await import('sharp')).default; + const svgPath = join(ROOT, 'src/web/public/icons/icon.svg'); + const outDir = join(ROOT, 'dist/web/public/icons'); + execSync(`mkdir -p "${outDir}"`, { cwd: ROOT, shell: true }); + + const svgBuf = readFileSync(svgPath); + + // Standard icons — full bleed + for (const size of [192, 512]) { + await sharp(svgBuf).resize(size, size).png().toFile(join(outDir, `icon-${size}x${size}.png`)); + } + + // Maskable icons — 80% center on background + for (const size of [192, 512]) { + const inner = Math.round(size * 0.8); + const pad = Math.round((size - inner) / 2); + const icon = await sharp(svgBuf).resize(inner, inner).png().toBuffer(); + await sharp({ + create: { width: size, height: size, channels: 4, background: { r: 10, g: 10, b: 10, alpha: 1 } }, + }).composite([{ input: icon, left: pad, top: pad }]).png().toFile(join(outDir, `icon-maskable-${size}x${size}.png`)); + } + + // Apple touch icon — 180x180, full bleed + await sharp(svgBuf).resize(180, 180).png().toFile(join(outDir, 'apple-touch-icon-180x180.png')); + + console.log('[build] generate PWA icons — done'); +} +``` + +- [ ] **Step 3: Run the build and verify icons are generated** + +Run: `npm run build` + +Verify: `ls -la dist/web/public/icons/` + +Expected: 5 PNG files: +``` +icon-192x192.png +icon-512x512.png +icon-maskable-192x192.png +icon-maskable-512x512.png +apple-touch-icon-180x180.png +``` + +- [ ] **Step 4: Commit** + +```bash +git add src/web/public/icons/icon.svg scripts/build.mjs +git commit -m "feat(pwa): add source SVG and icon generation build step" +``` + +--- + +### Task 2: Manifest & iOS Meta Tags + +**Files:** +- Modify: `src/web/public/manifest.json` +- Modify: `src/web/public/index.html` + +- [ ] **Step 1: Update the manifest** + +Replace the contents of `src/web/public/manifest.json` with: + +```json +{ + "name": "Codeman", + "short_name": "Codeman", + "description": "AI coding agent control plane — real-time monitoring dashboard", + "start_url": "/", + "display": "standalone", + "background_color": "#0a0a0a", + "theme_color": "#0a0a0a", + "scope": "/", + "id": "/", + "lang": "en", + "icons": [ + { "src": "/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" }, + { "src": "/icons/icon-maskable-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, + { "src": "/icons/icon-maskable-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} +``` + +- [ ] **Step 2: Add iOS meta tags to index.html** + +In `src/web/public/index.html`, after line 8 (``), add: + +```html + + + +``` + +- [ ] **Step 3: Build and verify** + +Run: `npm run build` + +Verify in `dist/web/public/index.html` that the iOS meta tags are present and the manifest link still works. + +- [ ] **Step 4: Commit** + +```bash +git add src/web/public/manifest.json src/web/public/index.html +git commit -m "feat(pwa): expand manifest with icons, add iOS meta tags" +``` + +--- + +### Task 3: Service Worker — Precache & Fetch Handler + +**Files:** +- Rewrite: `src/web/public/sw.js` + +- [ ] **Step 1: Rewrite sw.js with caching + existing push handlers** + +Replace the entire contents of `src/web/public/sw.js` with: + +```js +/** + * @fileoverview Codeman service worker — offline caching + Web Push notifications. + * + * Caching strategy: + * - Install: precache app shell assets (HTML, CSS, JS, vendor libs) + * - Fetch: cache-first for precached assets, NetworkFirst for /api/sessions, + * network-only for everything else (WebSocket, other APIs, push endpoints) + * - Activate: purge old caches + * + * Push notifications (unchanged from original): + * - Receives push events and displays OS-level notifications + * - Handles notification clicks to focus/open Codeman tab + * + * Update flow: + * - Listens for SKIP_WAITING message from app.js to activate immediately + */ + +const CACHE_VERSION = 1; +const SHELL_CACHE = `codeman-shell-v${CACHE_VERSION}`; +const API_CACHE = `codeman-api-v${CACHE_VERSION}`; + +const PRECACHE_URLS = [ + '/', + '/styles.css', + '/mobile.css', + '/constants.js', + '/feature-registry.js', + '/feature-tracker.js', + '/mobile-handlers.js', + '/voice-input.js', + '/notification-manager.js', + '/secret-detector.js', + '/keyboard-accessory.js', + '/app.js', + '/ralph-wizard.js', + '/api-client.js', + '/subagent-windows.js', + '/manifest.json', + '/icons/icon-192x192.png', + '/vendor/xterm.min.js', + '/vendor/xterm.css', + '/vendor/xterm-addon-fit.min.js', + '/vendor/xterm-addon-webgl.min.js', + '/vendor/xterm-addon-unicode11.min.js', + '/vendor/xterm-addon-search.min.js', +]; + +// ═══════════════════════════════════════════════════════════════ +// Lifecycle +// ═══════════════════════════════════════════════════════════════ + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(SHELL_CACHE).then((cache) => cache.addAll(PRECACHE_URLS)) + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all( + keys + .filter((k) => k !== SHELL_CACHE && k !== API_CACHE) + .map((k) => caches.delete(k)) + ) + ).then(() => self.clients.claim()) + ); +}); + +// ═══════════════════════════════════════════════════════════════ +// Fetch — caching strategies +// ═══════════════════════════════════════════════════════════════ + +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + // Never intercept non-GET or cross-origin requests + if (request.method !== 'GET' || url.origin !== self.location.origin) return; + + // NetworkFirst for /api/sessions (session list snapshot) + if (url.pathname === '/api/sessions') { + event.respondWith(networkFirstSessions(request)); + return; + } + + // Cache-first for precached app shell assets + if (isPrecached(url.pathname)) { + event.respondWith( + caches.match(request).then((cached) => cached || fetch(request)) + ); + return; + } + + // Everything else: network-only (WebSocket, other APIs, push endpoints) +}); + +function isPrecached(pathname) { + return PRECACHE_URLS.includes(pathname); +} + +async function networkFirstSessions(request) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + try { + const response = await fetch(request, { signal: controller.signal }); + clearTimeout(timeoutId); + // Cache successful responses + if (response.ok) { + const cache = await caches.open(API_CACHE); + cache.put(request, response.clone()); + } + return response; + } catch { + clearTimeout(timeoutId); + // Network failed — serve from cache with stale marker + const cached = await caches.match(request); + if (cached) { + // Clone response and add X-Codeman-Cached header so the app can detect stale data + const headers = new Headers(cached.headers); + headers.set('X-Codeman-Cached', 'true'); + return new Response(cached.body, { + status: cached.status, + statusText: cached.statusText, + headers, + }); + } + // Nothing cached — return network error + return new Response(JSON.stringify({ error: 'offline' }), { + status: 503, + headers: { 'Content-Type': 'application/json' }, + }); + } +} + +// ═══════════════════════════════════════════════════════════════ +// SW Update — SKIP_WAITING message from app.js +// ═══════════════════════════════════════════════════════════════ + +self.addEventListener('message', (event) => { + if (event.data?.type === 'SKIP_WAITING') { + self.skipWaiting(); + } +}); + +// ═══════════════════════════════════════════════════════════════ +// Web Push Notifications +// ═══════════════════════════════════════════════════════════════ + +self.addEventListener('push', (event) => { + if (!event.data) return; + + let payload; + try { + payload = event.data.json(); + } catch { + return; + } + + const { title, body, tag, sessionId, urgency, actions } = payload; + + const options = { + body: body || '', + tag: tag || 'codeman-default', + icon: '/icons/icon-192x192.png', + badge: '/icons/icon-192x192.png', + data: { sessionId, url: sessionId ? `/?session=${sessionId}` : '/' }, + renotify: true, + requireInteraction: urgency === 'critical', + }; + + if (actions && actions.length > 0) { + options.actions = actions; + } + + event.waitUntil( + self.registration.showNotification(title || 'Codeman', options) + ); +}); + +self.addEventListener('notificationclick', (event) => { + event.notification.close(); + + const { sessionId, url } = event.notification.data || {}; + const targetUrl = url || '/'; + + event.waitUntil( + self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => { + for (const client of clients) { + if (client.url.includes(self.location.origin)) { + client.postMessage({ + type: 'notification-click', + sessionId, + action: event.action || null, + }); + return client.focus(); + } + } + return self.clients.openWindow(targetUrl); + }) + ); +}); +``` + +Key changes from the original: +- Added `PRECACHE_URLS`, `SHELL_CACHE`, `API_CACHE` constants +- `install` event now precaches the app shell (instead of just `skipWaiting`) +- `activate` event purges old caches and claims clients +- New `fetch` event handler with cache-first for shell, NetworkFirst for sessions +- New `message` handler for `SKIP_WAITING` +- Push `icon`/`badge` now point to the real PNG instead of `/favicon.ico` +- Note: removed the unconditional `self.skipWaiting()` from install — the new SW waits until the app tells it to activate (via the SKIP_WAITING message), so active sessions aren't disrupted + +- [ ] **Step 2: Build and verify the SW is copied to dist** + +Run: `npm run build` + +Verify: `head -5 dist/web/public/sw.js` shows the new file header. + +- [ ] **Step 3: Commit** + +```bash +git add src/web/public/sw.js +git commit -m "feat(pwa): rewrite service worker with precache and offline session snapshot" +``` + +--- + +### Task 4: SW Update Toast in app.js + +**Files:** +- Modify: `src/web/public/app.js` + +- [ ] **Step 1: Update registerServiceWorker() with update detection** + +In `src/web/public/app.js`, replace the `registerServiceWorker()` method (lines 13277–13301) with: + +```js + registerServiceWorker() { + if (!('serviceWorker' in navigator)) return; + navigator.serviceWorker.register('/sw.js').then((reg) => { + this._swRegistration = reg; + + // Listen for messages from service worker (notification clicks) + navigator.serviceWorker.addEventListener('message', (event) => { + if (event.data?.type === 'notification-click') { + const { sessionId } = event.data; + if (sessionId && this.sessions.has(sessionId)) { + this.selectSession(sessionId); + } + window.focus(); + } + }); + + // SW update detection — show toast when new version is waiting + reg.addEventListener('updatefound', () => { + const newWorker = reg.installing; + if (!newWorker) return; + newWorker.addEventListener('statechange', () => { + // Only show toast for updates (not first install) + if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { + this._showSwUpdateToast(newWorker); + } + }); + }); + + // Listen for controller change (after SKIP_WAITING) and reload + navigator.serviceWorker.addEventListener('controllerchange', () => { + if (this._swReloading) return; + this._swReloading = true; + window.location.reload(); + }); + + // Check if already subscribed + reg.pushManager.getSubscription().then((sub) => { + if (sub) { + this._pushSubscription = sub; + this._updatePushUI(true); + } + }); + }).catch(() => { + // Service worker registration failed (likely not HTTPS) + }); + } + + _showSwUpdateToast(waitingWorker) { + if (this._swUpdateToastShown) return; + this._swUpdateToastShown = true; + this.showToast('New version available — tap to update', 'info', { + duration: 86400000, + action: { + label: 'Reload', + onClick: () => waitingWorker.postMessage({ type: 'SKIP_WAITING' }), + }, + }); + } +``` + +This preserves all existing behavior (notification click handling, push subscription check) and adds: +- `updatefound` listener on the registration +- `statechange` listener on the installing worker +- Toast shown only for updates (checks `navigator.serviceWorker.controller` exists) +- `controllerchange` listener for reload after SKIP_WAITING +- Guard `_swReloading` prevents double-reload + +- [ ] **Step 2: Build and verify** + +Run: `npm run build` + +Verify: `grep '_showSwUpdateToast' dist/web/public/app.js` returns a match. + +- [ ] **Step 3: Commit** + +```bash +git add src/web/public/app.js +git commit -m "feat(pwa): add service worker update detection and toast prompt" +``` + +--- + +### Task 5: Stale Session Data Indicator + +**Files:** +- Modify: `src/web/public/app.js` + +- [ ] **Step 1: Find the session-fetching code paths and add stale detection** + +There are two places in `app.js` that need the stale indicator. The main one is the initial session hydration from SSE/WebSocket. However, the session list in Codeman is hydrated via SSE events (`session-list`, `session-add`, etc.), not via a standalone `/api/sessions` fetch in the main app flow. The `/api/sessions` fetch calls at lines 23804 and 24216 are in the board module, not the main session drawer. + +The right approach: add a dedicated fetch to `/api/sessions` as a fallback when the SSE connection is down. But the app already handles SSE reconnection. The stale indicator should show when we detect we're offline and the SW served cached data. + +Add this method to the `CodemanApp` class, after the `registerServiceWorker()` / `_showSwUpdateToast()` methods: + +```js + /** + * Check if the last /api/sessions response came from the SW cache. + * Called after any fetch to /api/sessions to update the stale data indicator. + */ + _checkStaleSessionData(response) { + const isCached = response.headers.get('X-Codeman-Cached') === 'true'; + const indicator = document.getElementById('staleDataIndicator'); + if (isCached) { + const dateHeader = response.headers.get('Date'); + const timeStr = dateHeader ? new Date(dateHeader).toLocaleTimeString() : 'unknown'; + if (indicator) { + indicator.textContent = `Offline — last updated: ${timeStr}`; + indicator.style.display = ''; + } else { + this._createStaleIndicator(`Offline — last updated: ${timeStr}`); + } + } else if (indicator) { + indicator.style.display = 'none'; + } + } + + _createStaleIndicator(text) { + const el = document.createElement('div'); + el.id = 'staleDataIndicator'; + el.textContent = text; + el.style.cssText = 'padding:4px 12px;font-size:11px;color:#666;text-align:center;background:#111;border-bottom:1px solid #1a1a2e'; + // Insert at top of session drawer list + const drawer = document.querySelector('.session-drawer-list'); + if (drawer) { + drawer.parentNode.insertBefore(el, drawer); + } + } +``` + +- [ ] **Step 2: Wire up stale detection on the board's session fetch** + +In `app.js`, find the board refresh method at line ~24216: + +```js + fetch('/api/sessions').then(r => r.ok ? r.json() : []), +``` + +Replace the `/api/sessions` fetch in the board refresh with a version that checks for stale data. Find the line: + +```js + fetch('/api/sessions').then(r => r.ok ? r.json() : []), +``` + +Replace with: + +```js + fetch('/api/sessions').then(r => { if (typeof app !== 'undefined') app._checkStaleSessionData(r); return r.ok ? r.json() : []; }), +``` + +- [ ] **Step 3: Build and verify** + +Run: `npm run build` + +Verify: `grep 'staleDataIndicator' dist/web/public/app.js` returns matches. + +- [ ] **Step 4: Commit** + +```bash +git add src/web/public/app.js +git commit -m "feat(pwa): add offline stale session data indicator" +``` + +--- + +### Task 6: Build, Test & Final Commit + +**Files:** +- All modified files from Tasks 1–5 + +- [ ] **Step 1: Full clean build** + +```bash +npm run clean && npm run build +``` + +Expected: Build completes with no errors, including the new "generate PWA icons" step. + +- [ ] **Step 2: Verify all artifacts** + +```bash +# Icons generated +ls -la dist/web/public/icons/ + +# Manifest has icons +cat dist/web/public/manifest.json | grep -c icon + +# iOS meta tags present +grep 'apple-mobile-web-app-capable' dist/web/public/index.html + +# SW has precache +grep 'PRECACHE_URLS' dist/web/public/sw.js + +# SW update toast wired +grep '_showSwUpdateToast' dist/web/public/app.js + +# Stale indicator wired +grep 'staleDataIndicator' dist/web/public/app.js +``` + +Expected: All commands produce output (no empty results). + +- [ ] **Step 3: Run existing tests** + +```bash +npm test +``` + +Expected: All existing tests pass (this change doesn't touch any tested backend code). + +- [ ] **Step 4: Run lint and typecheck** + +```bash +npm run typecheck && npm run lint +``` + +Expected: No new errors (only `scripts/build.mjs` and frontend JS files were changed, neither is type-checked or linted by the current config). + +- [ ] **Step 5: Manual PWA verification** + +Start a dev instance and verify in Chrome DevTools: + +```bash +node dist/index.js web --port 3001 +``` + +Open `http://localhost:3001` in Chrome, then: + +1. DevTools → Application → Manifest: should show all icon sizes, no errors +2. DevTools → Application → Service Workers: should show the new SW as active +3. DevTools → Application → Cache Storage: should show `codeman-shell-v1` with all precached assets +4. Network tab → throttle to Offline → reload: app shell loads from cache +5. Check session list shows stale indicator when offline + +- [ ] **Step 6: Final commit (if any uncommitted changes from verification)** + +```bash +git status +# If clean, skip. If there are fixes from testing: +git add -u +git commit -m "fix(pwa): address issues found during verification" +``` diff --git a/docs/superpowers/specs/2026-04-25-board-case-id-design.md b/docs/superpowers/specs/2026-04-25-board-case-id-design.md new file mode 100644 index 00000000..c57602d1 --- /dev/null +++ b/docs/superpowers/specs/2026-04-25-board-case-id-design.md @@ -0,0 +1,43 @@ +# Board Work Item Creation — Case ID Assignment + +**Date**: 2026-04-25 +**Status**: Approved +**Scope**: Single-file change (app.js) + +## Problem + +Work items created from the Board UI via `openNewItemDialog()` never include a `caseId`. The orchestrator (orchestrator.ts:282-286) requires a non-null `caseId` to dispatch items, so UI-created items are permanently orphaned in the queue. + +## Solution + +Add a required "Case" `` populated with case names +3. Pre-select the currently active case from the toolbar (`localStorage.getItem('lastUsedCase')` or the toolbar's case selector value) +4. Include the selected value as `caseId` in the `POST /api/work-items` request body +5. No empty/none option — a case is required for the item to be dispatched + +### Placement + +The Case dropdown goes between the Title field and the Description textarea, matching the existing form layout style. + +### Data + +- Cases come from `GET /api/cases`, same endpoint the toolbar case selector uses +- `caseId` is the case directory name (string), not a numeric ID +- The REST API already accepts `caseId` — no backend changes needed + +## Files Changed + +| File | Change | +|------|--------| +| `src/web/public/app.js` | Add case `