Files
Shade/packages/shade-observer/src/static.ts

91 lines
2.9 KiB
TypeScript
Raw Normal View History

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 `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Shade Observer</title>
<style>
body { font-family: system-ui; max-width: 600px; margin: 80px auto; padding: 0 20px; color: #d4d4d4; background: #0a0a0a; }
h1 { color: #f7c948; }
code { background: #1a1a1a; padding: 2px 6px; border-radius: 4px; }
a { color: #f7c948; }
</style>
</head>
<body>
<h1>Shade Observer</h1>
<p>The dashboard SPA hasn't been built yet. The observer API is running, but there's no UI bundled.</p>
<p>To build the dashboard:</p>
<pre><code>cd packages/shade-dashboard && bun run build</code></pre>
<p>Then re-run the observer.</p>
<h2>API endpoints</h2>
<ul>
<li><code>GET /api/state</code> current snapshot (requires <code>Authorization: Bearer ...</code>)</li>
<li><code>GET /api/events</code> SSE stream of live events</li>
<li><code>GET /health</code> health check (no auth)</li>
</ul>
</body>
</html>`;
}