feat: implement container logs modal, service diagnostics inspector, and directory sizer tool
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s

This commit is contained in:
Kroonk
2026-05-21 18:49:59 +02:00
parent 6ff401ad74
commit 0a044bbb92
4 changed files with 649 additions and 7 deletions

185
server.js
View File

@@ -509,6 +509,127 @@ async function getDisks() {
return disks;
}
function isPathAllowed(userPath) {
if (!userPath) return false;
const resolved = path.resolve(userPath);
return resolved.startsWith('/host/volume1') || resolved.startsWith('/host/volume2') || resolved === '/';
}
async function getDirectorySize(dirPath, maxDepth = 3, currentDepth = 0) {
let size = 0;
try {
const stats = await fsp.lstat(dirPath);
if (stats.isSymbolicLink()) return 0;
if (stats.isFile()) {
return stats.size;
}
if (stats.isDirectory()) {
if (currentDepth >= maxDepth) return 0;
const files = await fsp.readdir(dirPath, { withFileTypes: true });
const promises = files.map(async (file) => {
const fullPath = path.join(dirPath, file.name);
if (file.isSymbolicLink()) return 0;
if (file.isDirectory()) {
return getDirectorySize(fullPath, maxDepth, currentDepth + 1);
}
if (file.isFile()) {
try {
const fileStats = await fsp.lstat(fullPath);
return fileStats.size;
} catch {
return 0;
}
}
return 0;
});
const sizes = await Promise.all(promises);
size = sizes.reduce((acc, curr) => acc + curr, 0);
}
} catch (err) {
// Ignore errors
}
return size;
}
async function scanDirectoryChildren(dirPath) {
if (!isPathAllowed(dirPath)) {
throw new Error('Unzulässiger Pfad. Zugriff verweigert.');
}
const files = await fsp.readdir(dirPath, { withFileTypes: true });
const results = [];
const promises = files.map(async (file) => {
const fullPath = path.join(dirPath, file.name);
try {
if (file.isSymbolicLink()) {
results.push({ name: file.name, path: fullPath, isDirectory: false, size: 0, isLink: true });
return;
}
if (file.isDirectory()) {
const dirSize = await getDirectorySize(fullPath, 2);
results.push({ name: file.name, path: fullPath, isDirectory: true, size: dirSize });
} else {
const stats = await fsp.lstat(fullPath);
results.push({ name: file.name, path: fullPath, isDirectory: false, size: stats.size });
}
} catch {
results.push({ name: file.name, path: fullPath, isDirectory: file.isDirectory(), size: 0, error: true });
}
});
await Promise.all(promises);
results.sort((a, b) => b.size - a.size);
return results;
}
function inspectService(urlStr) {
return new Promise((resolve) => {
try {
const parsed = new URL(urlStr);
const client = parsed.protocol === 'https:' ? https : http;
const start = Date.now();
const req = client.get(parsed.href, {
timeout: 4000,
headers: { 'User-Agent': 'MischLabs-Diagnostics/1.0' }
}, (res) => {
const chunks = [];
let bodyLength = 0;
res.on('data', (chunk) => {
if (bodyLength < 2000) {
chunks.push(chunk);
bodyLength += chunk.length;
}
});
res.on('end', () => {
const duration = Date.now() - start;
const bodyPreview = Buffer.concat(chunks).toString('utf8').slice(0, 1000);
resolve({
ok: res.statusCode >= 200 && res.statusCode < 400,
statusCode: res.statusCode,
statusMessage: res.statusMessage,
latency: duration,
headers: res.headers,
bodyPreview: bodyPreview
});
});
});
req.on('timeout', () => {
req.destroy();
resolve({ ok: false, error: 'Timeout nach 4 Sekunden', latency: 4000 });
});
req.on('error', (err) => {
resolve({ ok: false, error: err.message, latency: Date.now() - start });
});
} catch (err) {
resolve({ ok: false, error: err.message, latency: 0 });
}
});
}
function parseWatchtower(lines) {
const sessions = [];
let current = null;
@@ -828,6 +949,70 @@ const server = http.createServer(async (req, res) => {
return;
}
const containerLogsMatch = req.url?.match(/^\/api\/containers\/([^/?#]+)\/logs(?:[?#].*)?$/);
if (containerLogsMatch) {
if (req.method !== 'GET') {
json(res, 405, { error: 'Method not allowed' });
return;
}
try {
await verifyAdminRequest(req);
const containerName = decodeURIComponent(containerLogsMatch[1]);
const urlParams = new URL(req.url, 'http://localhost').searchParams;
const tail = Number(urlParams.get('tail') || 150);
const logs = await dockerLogs(containerName, tail);
json(res, 200, { name: containerName, logs });
} catch (error) {
json(res, error.statusCode || 500, { error: error.message });
}
return;
}
if (req.url?.startsWith('/api/services/inspect')) {
if (req.method !== 'GET') {
json(res, 405, { error: 'Method not allowed' });
return;
}
try {
await verifyAdminRequest(req);
const urlParams = new URL(req.url, 'http://localhost').searchParams;
const inspectUrl = urlParams.get('url');
if (!inspectUrl) {
json(res, 400, { error: 'Missing url parameter' });
return;
}
const data = await inspectService(inspectUrl);
json(res, 200, data);
} catch (error) {
json(res, error.statusCode || 500, { error: error.message });
}
return;
}
if (req.url?.startsWith('/api/disks/scan-folder')) {
if (req.method !== 'GET') {
json(res, 405, { error: 'Method not allowed' });
return;
}
try {
await verifyAdminRequest(req);
const urlParams = new URL(req.url, 'http://localhost').searchParams;
const scanPath = urlParams.get('path');
if (!scanPath) {
json(res, 400, { error: 'Missing path parameter' });
return;
}
const data = await scanDirectoryChildren(scanPath);
json(res, 200, { path: scanPath, children: data });
} catch (error) {
json(res, error.statusCode || 500, { error: error.message });
}
return;
}
await serveStatic(req, res);
});