116 lines
4.0 KiB
JavaScript
116 lines
4.0 KiB
JavaScript
#!/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';
|
|
|
|
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 });
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/reload') {
|
|
ledger = resolveAccountOwnerHints(await loadLedger(ledgerPath));
|
|
graph = buildMoneyGraph(ledger);
|
|
return sendJson(res, { reloaded: true, summary: summarizeLedger(ledger, graph) });
|
|
}
|
|
|
|
if (req.method === 'GET' && url.pathname === '/summary') {
|
|
return sendJson(res, summarizeLedger(ledger, graph));
|
|
}
|
|
|
|
if (req.method === 'GET' && url.pathname === '/entities') {
|
|
return sendJson(res, { entities: ledger.entities });
|
|
}
|
|
|
|
if (req.method === 'GET' && url.pathname === '/accounts') {
|
|
return sendJson(res, { accounts: ledger.accounts });
|
|
}
|
|
|
|
if (req.method === 'GET' && url.pathname === '/movements') {
|
|
return sendJson(res, { movements: ledger.movements });
|
|
}
|
|
|
|
const originMatch = url.pathname.match(/^\/nodes\/(.+)\/origin-tree$/);
|
|
if (req.method === 'GET' && originMatch) {
|
|
return sendJson(res, originTree(graph, decodeURIComponent(originMatch[1])));
|
|
}
|
|
|
|
const destinationMatch = url.pathname.match(/^\/nodes\/(.+)\/destination-tree$/);
|
|
if (req.method === 'GET' && destinationMatch) {
|
|
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);
|
|
}
|
|
});
|
|
|
|
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) {
|
|
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
res.end(JSON.stringify(body, null, 2));
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const parsed = {};
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
if (argv[i].startsWith('--')) {
|
|
parsed[argv[i].slice(2)] = argv[i + 1];
|
|
i += 1;
|
|
}
|
|
}
|
|
return parsed;
|
|
}
|