Full Redesign of p2ns.admin

This commit is contained in:
Raven Scott
2026-05-30 19:12:35 -04:00
parent 43f69a59a7
commit 9974202c39
34 changed files with 4160 additions and 688 deletions
@@ -0,0 +1,24 @@
const { logError } = require('../../../infrastructure/logger');
const { collectAdminAlerts } = require('../../alerts-collector');
async function handleAlertsRoutes(req, res) {
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
const method = req.method;
if (method === 'GET' && urlPath === '/api/admin/alerts') {
try {
const payload = await collectAdminAlerts();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(payload));
} catch (err) {
logError('Admin', `Failed to collect admin alerts: ${err.message}`);
res.writeHead(500);
res.end(JSON.stringify({ error: 'Failed to collect admin alerts' }));
}
return true;
}
return false;
}
module.exports = { handleAlertsRoutes };
@@ -7,7 +7,7 @@ const { validateHolesailClient } = require('../../../infrastructure/validation')
const { createInterfaceForDomain } = require('../../../networking/virtual_interfaces');
const { logDebug, logError, logInfo, logWarn } = require('../../../infrastructure/logger');
const { startHolesailServer, saveHolesailServers } = require('../holesail-servers');
const { startForkedHolesailClient, saveHolesailClients } = require('../holesail-clients');
const { startForkedHolesailClient, saveHolesailClients, getHolesailClientStatus } = require('../holesail-clients');
const { ensurePortFree } = require('../port-management');
const { broadcast } = require('../websocket');
const { getConsensusState, getClaimClients, updateClaimClients } = require('../../../core/core');
@@ -42,19 +42,8 @@ async function handleHolesailRoutes(req, res) {
if (method === 'GET' && urlPath === '/api/holesail-clients') {
try {
const clients = Array.from(state.holesailClientOpts.entries()).map(([id, opts]) => {
const child = state.holesailClientChildren.get(id);
const info = state.holesailClientInfos.get(id) || {};
const key = `${opts.domain}:${opts.port}`;
const isHolesailActive = state.holesails.has(key);
const isChildRunning = child && !child.killed;
let status = 'stopped';
if (isChildRunning && isHolesailActive && info.state !== 'error') {
status = 'running';
} else if (isChildRunning || isHolesailActive) {
status = 'starting';
} else if (info.state === 'error') {
status = 'error';
}
const status = getHolesailClientStatus(id, opts, info);
return { id, opts, info: { ...info, state: status } };
});
res.writeHead(200, { 'Content-Type': 'application/json' });
+3 -1
View File
@@ -16,6 +16,7 @@ const { handleDiagnosticsRoutes } = require('./diagnostics');
const { handleConsensusRoutes } = require('./consensus');
const { handlePluginsRoutes } = require('./plugins');
const { handleLogsRoutes } = require('./logs');
const { handleAlertsRoutes } = require('./alerts');
async function handleAdminRequest(req, res) {
const url = new URL(req.url, `https://${req.headers.host}`);
@@ -54,9 +55,10 @@ async function handleAdminRequest(req, res) {
if (await handleConsensusRoutes(req, res)) return;
if (await handlePluginsRoutes(req, res)) return;
if (await handleLogsRoutes(req, res)) return;
if (await handleAlertsRoutes(req, res)) return;
// No route matched
res.writeHead(404);
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Not Found');
}
@@ -1,7 +1,22 @@
const state = require('../../../infrastructure/state');
const { cleanupInterfaces } = require('../../../maintenance/cleanup');
const { logError } = require('../../../infrastructure/logger');
const { broadcast } = require('../websocket');
const { scheduleInterfacesBroadcast } = require('../interfaces-broadcast');
const { buildInterfacesResponse, removeOrphanedInterfaceIp } = require('../../interfaces-data');
function readJsonBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
try {
resolve(body ? JSON.parse(body) : {});
} catch (err) {
reject(new Error('Invalid JSON body'));
}
});
req.on('error', reject);
});
}
async function handleInterfacesRoutes(req, res) {
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
@@ -9,9 +24,9 @@ async function handleInterfacesRoutes(req, res) {
if (method === 'GET' && urlPath === '/api/interfaces') {
try {
const interfaces = Array.from(state.domainToIPMap.entries()).map(([domain, ip]) => ({ domain, ip }));
const payload = await buildInterfacesResponse();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(interfaces));
res.end(JSON.stringify(payload));
} catch (err) {
logError('Admin', `Failed to fetch interfaces: ${err.message}`);
res.writeHead(500);
@@ -20,10 +35,25 @@ async function handleInterfacesRoutes(req, res) {
return true;
}
if (method === 'POST' && urlPath === '/api/interfaces/remove-ip') {
try {
const { ip } = await readJsonBody(req);
const removedIp = await removeOrphanedInterfaceIp(ip);
scheduleInterfacesBroadcast();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, ip: removedIp }));
} catch (err) {
logError('Admin', `Failed to remove orphaned IP: ${err.message}`);
res.writeHead(400);
res.end(JSON.stringify({ error: err.message }));
}
return true;
}
if (method === 'POST' && urlPath === '/api/cleanup-interfaces') {
try {
await cleanupInterfaces();
broadcast({ type: 'update-interfaces' });
scheduleInterfacesBroadcast();
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('OK');
} catch (err) {
@@ -38,4 +68,3 @@ async function handleInterfacesRoutes(req, res) {
}
module.exports = { handleInterfacesRoutes };
+71 -54
View File
@@ -2,95 +2,112 @@ const fs = require('fs').promises;
const pathModule = require('path');
const { logDebug, logError } = require('../../../infrastructure/logger');
const ADMIN_FRONTEND_DIR = pathModule.join(__dirname, '..', '..', 'admin-frontend');
const TAILWIND_CSS = pathModule.join(__dirname, '..', '..', '..', 'css', 'tailwind.css');
const MIME_TYPES = {
'.css': 'text/css; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.html': 'text/html; charset=utf-8',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.png': 'image/png',
'.json': 'application/json',
'.webmanifest': 'application/manifest+json'
};
function isPathInsideDir(candidate, baseDir) {
const resolvedBase = pathModule.resolve(baseDir);
const resolvedCandidate = pathModule.resolve(candidate);
return resolvedCandidate === resolvedBase
|| resolvedCandidate.startsWith(resolvedBase + pathModule.sep);
}
async function serveFile(filePath, res) {
const ext = pathModule.extname(filePath).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
const body = await fs.readFile(filePath);
res.writeHead(200, { 'Content-Type': contentType });
res.end(body);
}
async function serveAdminFrontendAsset(urlPath, res) {
if (!/^\/(?:ui\/[\w.-]+\.js|[\w.-]+\.(?:css|js|svg|ico|png|json|webmanifest))$/.test(urlPath)) {
return false;
}
const relativePath = urlPath.replace(/^\//, '');
const filePath = pathModule.join(ADMIN_FRONTEND_DIR, relativePath);
if (!isPathInsideDir(filePath, ADMIN_FRONTEND_DIR)) {
return false;
}
try {
await serveFile(filePath, res);
return true;
} catch (err) {
if (err.code === 'ENOENT') {
return false;
}
logError('Admin', `Failed to serve ${urlPath}: ${err.message}`);
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Failed to load asset');
return true;
}
}
async function handleStaticRoutes(req, res) {
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
const method = req.method;
const tabs = ['domains', 'host', 'local-dns', 'entries', 'peers', 'certs', 'interfaces', 'logs', 'settings', 'stats'];
const tabs = ['domains', 'host', 'local-dns', 'entries', 'peers', 'certs', 'interfaces', 'logs', 'settings', 'stats', 'backups', 'plugins'];
if (method === 'GET' && urlPath.startsWith('/') && tabs.includes(urlPath.substring(1))) {
res.writeHead(302, { 'Location': `/#${urlPath.substring(1)}` });
res.end();
return true;
}
if (method === 'GET' && urlPath === '/') {
logDebug('Admin', 'Serving admin panel HTML');
try {
const html = await fs.readFile(pathModule.join(__dirname, '..', '..', 'admin-frontend', 'index.html'), 'utf8');
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(html);
await serveFile(pathModule.join(ADMIN_FRONTEND_DIR, 'index.html'), res);
} catch (err) {
logError('Admin', `Failed to serve index.html: ${err.message}`);
res.writeHead(500);
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Failed to load admin panel');
}
return true;
}
if (method === 'GET' && urlPath === '/tailwind.css') {
try {
const css = await fs.readFile(pathModule.join(__dirname, '..', '..', '..', 'css', 'tailwind.css'), 'utf8');
res.writeHead(200, { 'Content-Type': 'text/css' });
res.end(css);
await serveFile(TAILWIND_CSS, res);
} catch (err) {
logError('Admin', `Failed to serve tailwind.css: ${err.message}`);
res.writeHead(500);
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Failed to load Tailwind CSS');
}
return true;
}
if (method === 'GET' && urlPath === '/styles.css') {
try {
const css = await fs.readFile(pathModule.join(__dirname, '..', '..', 'admin-frontend', 'styles.css'), 'utf8');
res.writeHead(200, { 'Content-Type': 'text/css' });
res.end(css);
} catch (err) {
logError('Admin', `Failed to serve styles.css: ${err.message}`);
res.writeHead(500);
res.end('Failed to load styles');
if (method === 'GET') {
const served = await serveAdminFrontendAsset(urlPath, res);
if (served) {
return true;
}
return true;
}
// Serve frontend JavaScript files
if (method === 'GET' && (urlPath === '/admin.js' || urlPath === '/utils.js' || urlPath === '/ws-client.js')) {
if (method === 'GET' && urlPath === '/favicon.ico') {
try {
const fileName = urlPath.substring(1); // Remove leading '/'
const js = await fs.readFile(pathModule.join(__dirname, '..', '..', 'admin-frontend', fileName), 'utf8');
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.end(js);
await serveFile(pathModule.join(ADMIN_FRONTEND_DIR, 'favicon.svg'), res);
} catch (err) {
logError('Admin', `Failed to serve ${urlPath}: ${err.message}`);
res.writeHead(500);
res.end('Failed to load script');
}
return true;
}
// Serve UI module files
if (method === 'GET' && urlPath.startsWith('/ui/') && urlPath.endsWith('.js')) {
try {
const fileName = urlPath.substring(1); // Remove leading '/' -> 'ui/filename.js'
const js = await fs.readFile(pathModule.join(__dirname, '..', '..', 'admin-frontend', fileName), 'utf8');
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.end(js);
} catch (err) {
logError('Admin', `Failed to serve ${urlPath}: ${err.message}`);
res.writeHead(404);
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Not Found');
}
return true;
}
if (urlPath === '/favicon.ico') {
res.writeHead(404);
res.end('Not Found');
return true;
}
return false;
}
module.exports = { handleStaticRoutes };