- server.ts: Hono server with basic auth, GET/PUT/DELETE /api/* endpoints - defaults.json: extracted board defaults from index.html - Dockerfile: containerized for Coolify deployment - index.html: all state-layer rewritten from localStorage to fetch API Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
100 lines
3.7 KiB
TypeScript
100 lines
3.7 KiB
TypeScript
import { Hono } from 'hono';
|
|
import { basicAuth } from 'hono/basic-auth';
|
|
import { serveStatic } from 'hono/bun';
|
|
import { existsSync, mkdirSync, renameSync, unlinkSync } from 'fs';
|
|
import { readdir } from 'fs/promises';
|
|
import path from 'path';
|
|
import defaults from './defaults.json';
|
|
|
|
const PORT = parseInt(Bun.env.PORT || '3000');
|
|
export const DATA_DIR = Bun.env.DATA_DIR || path.join(import.meta.dir, 'data');
|
|
const BOARDS_DIR = path.join(DATA_DIR, 'boards');
|
|
|
|
mkdirSync(BOARDS_DIR, { recursive: true });
|
|
|
|
async function atomicWrite(filePath: string, content: string) {
|
|
const tmp = filePath + '.tmp';
|
|
await Bun.write(tmp, content);
|
|
renameSync(tmp, filePath);
|
|
}
|
|
|
|
async function bootstrap() {
|
|
if (existsSync(path.join(DATA_DIR, 'meta.json'))) return;
|
|
await atomicWrite(path.join(DATA_DIR, 'meta.json'), JSON.stringify({
|
|
groups: defaults.groups,
|
|
boardOrder: {},
|
|
}, null, 2));
|
|
await atomicWrite(path.join(DATA_DIR, 'ideas.json'), JSON.stringify({ ideas: [] }, null, 2));
|
|
for (const [id, board] of Object.entries(defaults.boards)) {
|
|
const meta = (defaults.boardMeta as Record<string, { group: string; color: string; ring: string }>)[id]
|
|
|| { group: 'Privat', color: '#7c6af7', ring: '?' };
|
|
await atomicWrite(path.join(BOARDS_DIR, `${id}.json`), JSON.stringify({ ...board, id, ...meta }, null, 2));
|
|
}
|
|
console.log('Bootstrap: initialisiert mit', Object.keys(defaults.boards).length, 'Boards');
|
|
}
|
|
|
|
await bootstrap();
|
|
|
|
const app = new Hono();
|
|
|
|
if (Bun.env.KANBAN_USER && Bun.env.KANBAN_PASS) {
|
|
app.use('*', basicAuth({ username: Bun.env.KANBAN_USER!, password: Bun.env.KANBAN_PASS! }));
|
|
}
|
|
|
|
app.get('/api/state', async (c) => {
|
|
const metaPath = path.join(DATA_DIR, 'meta.json');
|
|
const ideasPath = path.join(DATA_DIR, 'ideas.json');
|
|
const meta = existsSync(metaPath)
|
|
? JSON.parse(await Bun.file(metaPath).text())
|
|
: { groups: defaults.groups, boardOrder: {} };
|
|
const ideas = existsSync(ideasPath)
|
|
? (JSON.parse(await Bun.file(ideasPath).text())).ideas ?? []
|
|
: [];
|
|
const boards: Record<string, unknown> = {};
|
|
const files = await readdir(BOARDS_DIR).catch(() => [] as string[]);
|
|
for (const f of files.filter(f => f.endsWith('.json'))) {
|
|
const id = f.slice(0, -5);
|
|
boards[id] = JSON.parse(await Bun.file(path.join(BOARDS_DIR, f)).text());
|
|
}
|
|
return c.json({ meta, boards, ideas });
|
|
});
|
|
|
|
app.put('/api/boards/:id', async (c) => {
|
|
const id = c.req.param('id');
|
|
if (!/^[a-z0-9_-]+$/i.test(id)) return c.json({ error: 'invalid id' }, 400);
|
|
const body = await c.req.text();
|
|
try { JSON.parse(body); } catch { return c.json({ error: 'invalid json' }, 400); }
|
|
await atomicWrite(path.join(BOARDS_DIR, `${id}.json`), body);
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
app.delete('/api/boards/:id', async (c) => {
|
|
const id = c.req.param('id');
|
|
const fp = path.join(BOARDS_DIR, `${id}.json`);
|
|
if (existsSync(fp)) unlinkSync(fp);
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
app.put('/api/meta', async (c) => {
|
|
const body = await c.req.text();
|
|
try { JSON.parse(body); } catch { return c.json({ error: 'invalid json' }, 400); }
|
|
await atomicWrite(path.join(DATA_DIR, 'meta.json'), body);
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
app.put('/api/ideas', async (c) => {
|
|
const body = await c.req.text();
|
|
try { JSON.parse(body); } catch { return c.json({ error: 'invalid json' }, 400); }
|
|
await atomicWrite(path.join(DATA_DIR, 'ideas.json'), body);
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
app.use('/assets/*', serveStatic({ root: './' }));
|
|
|
|
app.get('*', async () => new Response(Bun.file(path.join(import.meta.dir, 'index.html')), {
|
|
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
}));
|
|
|
|
Bun.serve({ fetch: app.fetch, port: PORT });
|
|
console.log(`Kanban läuft auf http://localhost:${PORT}`);
|