diff --git a/src/server.js b/src/server.js index d2d196d..90d1361 100644 --- a/src/server.js +++ b/src/server.js @@ -1,5 +1,7 @@ #!/usr/bin/env node import http from 'node:http'; +import fs from 'node:fs/promises'; +import path from 'node:path'; import { URL } from 'node:url'; import { buildMoneyGraph, destinationTree, originTree, summarizeLedger } from './moneyGraph.js'; import { loadLedger, resolveAccountOwnerHints } from './ledgerStore.js'; @@ -8,14 +10,28 @@ const args = parseArgs(process.argv.slice(2)); const ledgerPath = args.ledger || 'data/example-ledger.json'; const port = Number(args.port || 3910); const host = args.host || '127.0.0.1'; +const webDir = path.resolve(args.web || 'web'); let ledger = resolveAccountOwnerHints(await loadLedger(ledgerPath)); let graph = buildMoneyGraph(ledger); +const MIME = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.ico': 'image/x-icon', +}; + const server = http.createServer(async (req, res) => { try { const url = new URL(req.url, `http://${req.headers.host}`); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } + if (req.method === 'GET' && url.pathname === '/health') { return sendJson(res, { status: 'ok', service: 'kua-money-trace', ledger: ledgerPath }); } @@ -52,6 +68,24 @@ const server = http.createServer(async (req, res) => { return sendJson(res, destinationTree(graph, decodeURIComponent(destinationMatch[1]))); } + // static files from web/ + if (req.method === 'GET') { + const filePath = path.join(webDir, url.pathname === '/' ? 'dashboard.html' : url.pathname); + const realPath = path.resolve(filePath); + if (!realPath.startsWith(webDir + path.sep) && realPath !== webDir) { + return sendJson(res, { error: 'forbidden' }, 403); + } + try { + const data = await fs.readFile(realPath); + const ext = path.extname(realPath); + res.writeHead(200, { 'content-type': MIME[ext] || 'application/octet-stream' }); + res.end(data); + return; + } catch { + // fall through to 404 + } + } + sendJson(res, { error: 'not found' }, 404); } catch (error) { sendJson(res, { error: error.message }, 500); @@ -60,6 +94,8 @@ const server = http.createServer(async (req, res) => { server.listen(port, host, () => { console.log(`kua-money-trace listening on http://${host}:${port}`); + console.log(` dashboard: http://${host}:${port}/dashboard.html`); + console.log(` ledger: ${ledgerPath}`); }); function sendJson(res, body, status = 200) { diff --git a/web/dashboard.html b/web/dashboard.html new file mode 100644 index 0000000..9a0f303 --- /dev/null +++ b/web/dashboard.html @@ -0,0 +1,553 @@ + + + + + +money-trace · dashboard + + + + +
+

money‑trace · dashboard

+ +
+ + + + +
+
+ +
+
+
+ + +
+
Entidades
+
Cuentas
+
Movimientos
+
Documentos
+
Eventos
+
Links
+
Ingresos reales
+
Egresos reales
+
+ + +
+
+

Entidades

+
Cargando…
+
+
+

Movimientos por tipo económico

+
Cargando…
+
+
+ + +
+
+

Movimientos

+
+ + + +
+
+
+ + + + + + + + + + + +
FechaCuentaDirMontoTipo económicoDescripción / ContraparteÁrbol
+ +
+
+
+ + +
+
+

Árbol de trazabilidad

+ +
+
+ + +
+
Selecciona un movimiento.
+
+
+ + + +