rewrite from monolithic hono jsx to react 19 spa with tanstack router + hono json api backend. add scan, review, execute, nodes, and setup pages. multi-stage dockerfile (node for vite build, bun for runtime). previously, server/ and src/shared/lib/ were silently excluded by global gitignore patterns (/server/ from emacs, lib/ from python). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
75 lines
3.0 KiB
TypeScript
75 lines
3.0 KiB
TypeScript
import { Hono } from 'hono';
|
|
import { getDb } from '../db/index';
|
|
import { testConnection } from '../services/ssh';
|
|
import type { Node } from '../types';
|
|
|
|
const app = new Hono();
|
|
|
|
app.get('/', (c) => {
|
|
const db = getDb();
|
|
const nodes = db.prepare('SELECT * FROM nodes ORDER BY name').all() as Node[];
|
|
return c.json({ nodes });
|
|
});
|
|
|
|
app.post('/', async (c) => {
|
|
const db = getDb();
|
|
const contentType = c.req.header('Content-Type') ?? '';
|
|
let name: string, host: string, port: number, username: string, ffmpegPath: string, workDir: string, privateKey: string;
|
|
|
|
// Support both multipart (file upload) and JSON
|
|
if (contentType.includes('multipart/form-data')) {
|
|
const body = await c.req.formData();
|
|
name = body.get('name') as string;
|
|
host = body.get('host') as string;
|
|
port = Number(body.get('port') ?? '22');
|
|
username = body.get('username') as string;
|
|
ffmpegPath = (body.get('ffmpeg_path') as string) || 'ffmpeg';
|
|
workDir = (body.get('work_dir') as string) || '/tmp';
|
|
const keyFile = body.get('private_key') as File | null;
|
|
if (!name || !host || !username || !keyFile) return c.json({ ok: false, error: 'All fields are required' }, 400);
|
|
privateKey = await keyFile.text();
|
|
} else {
|
|
const body = await c.req.json<{ name: string; host: string; port?: number; username: string; ffmpeg_path?: string; work_dir?: string; private_key: string }>();
|
|
name = body.name; host = body.host; port = body.port ?? 22; username = body.username;
|
|
ffmpegPath = body.ffmpeg_path || 'ffmpeg'; workDir = body.work_dir || '/tmp'; privateKey = body.private_key;
|
|
if (!name || !host || !username || !privateKey) return c.json({ ok: false, error: 'All fields are required' }, 400);
|
|
}
|
|
|
|
try {
|
|
db.prepare('INSERT INTO nodes (name, host, port, username, private_key, ffmpeg_path, work_dir) VALUES (?, ?, ?, ?, ?, ?, ?)')
|
|
.run(name, host, port, username, privateKey, ffmpegPath, workDir);
|
|
} catch (e) {
|
|
if (String(e).includes('UNIQUE')) return c.json({ ok: false, error: `A node named "${name}" already exists` }, 409);
|
|
throw e;
|
|
}
|
|
|
|
const nodes = db.prepare('SELECT * FROM nodes ORDER BY name').all() as Node[];
|
|
return c.json({ ok: true, nodes });
|
|
});
|
|
|
|
app.delete('/:id', (c) => {
|
|
const db = getDb();
|
|
db.prepare('DELETE FROM nodes WHERE id = ?').run(Number(c.req.param('id')));
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
// Legacy POST delete for HTML-form compat (may be removed later)
|
|
app.post('/:id/delete', (c) => {
|
|
const db = getDb();
|
|
db.prepare('DELETE FROM nodes WHERE id = ?').run(Number(c.req.param('id')));
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
app.post('/:id/test', async (c) => {
|
|
const db = getDb();
|
|
const id = Number(c.req.param('id'));
|
|
const node = db.prepare('SELECT * FROM nodes WHERE id = ?').get(id) as Node | undefined;
|
|
if (!node) return c.notFound();
|
|
const result = await testConnection(node);
|
|
const status = result.ok ? 'ok' : `error: ${result.error}`;
|
|
db.prepare("UPDATE nodes SET status = ?, last_checked_at = datetime('now') WHERE id = ?").run(status, id);
|
|
return c.json({ ok: result.ok, status, error: result.error });
|
|
});
|
|
|
|
export default app;
|