import { Hono } from 'hono'; import { join } from 'path'; import { existsSync, readFileSync, statSync } from 'fs'; /** * Serve the bundled dashboard SPA from /dashboard/. * * Looks for dist/ in the @shade/observer package directory. * Falls back to a placeholder page if no build is present. */ export function createStaticRoutes(distDir: string): Hono { const app = new Hono(); app.get('/dashboard', (c) => c.redirect('/dashboard/')); app.get('/dashboard/*', async (c) => { const url = new URL(c.req.url); let path = url.pathname.replace(/^\/dashboard\/?/, '') || 'index.html'; // Prevent path traversal if (path.includes('..')) { return c.text('Forbidden', 403); } const fullPath = join(distDir, path); if (!existsSync(fullPath) || !statSync(fullPath).isFile()) { // Fall back to index.html for SPA routing const indexPath = join(distDir, 'index.html'); if (!existsSync(indexPath)) { return c.html(placeholderHtml()); } const content = readFileSync(indexPath); c.header('Content-Type', 'text/html; charset=utf-8'); return c.body(content as any); } const content = readFileSync(fullPath); const ct = contentTypeFor(path); c.header('Content-Type', ct); if (path.endsWith('.html')) { c.header('Cache-Control', 'no-cache'); } else { c.header('Cache-Control', 'public, max-age=3600'); } return c.body(content as any); }); return app; } function contentTypeFor(path: string): string { if (path.endsWith('.html')) return 'text/html; charset=utf-8'; if (path.endsWith('.js')) return 'application/javascript; charset=utf-8'; if (path.endsWith('.css')) return 'text/css; charset=utf-8'; if (path.endsWith('.json')) return 'application/json; charset=utf-8'; if (path.endsWith('.svg')) return 'image/svg+xml'; if (path.endsWith('.png')) return 'image/png'; if (path.endsWith('.woff2')) return 'font/woff2'; return 'application/octet-stream'; } function placeholderHtml(): string { return `
The dashboard SPA hasn't been built yet. The observer API is running, but there's no UI bundled.
To build the dashboard:
cd packages/shade-dashboard && bun run build
Then re-run the observer.
GET /api/state — current snapshot (requires Authorization: Bearer ...)GET /api/events — SSE stream of live eventsGET /health — health check (no auth)