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)[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 = {}; 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}`);