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

294
admin.js
View File

@@ -387,7 +387,10 @@ function renderServicesStatus(services = []) {
return `
<a class="status-board-item ${statusClass}" href="${item.url}" target="_blank" rel="noopener noreferrer" title="${tooltip}">
<h4>${item.name}</h4>
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
<h4 style="margin: 0; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; flex-grow: 1;">${item.name}</h4>
<span class="service-inspect-trigger" data-url="${item.url}" data-name="${item.name}" title="Live-Diagnose ausführen" style="font-size: 0.85rem; cursor: pointer; padding: 2px 4px; background: rgba(255,255,255,0.08); border-radius: 4px; margin-left: 6px; transition: transform 0.1s ease; user-select: none;">🔍</span>
</div>
<div class="status-board-item-bar">
<div class="status-board-item-bar-inner"></div>
</div>
@@ -395,6 +398,16 @@ function renderServicesStatus(services = []) {
`;
}).join('');
servicesStatusBoard.querySelectorAll('.service-inspect-trigger').forEach((trigger) => {
trigger.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const url = trigger.dataset.url;
const name = trigger.dataset.name;
openServiceInspectModal(url, name);
});
});
const miniServices = document.getElementById('mini-services-status');
if (miniServices) {
let ledHtml = itemsToRender.map(item => {
@@ -536,12 +549,21 @@ function renderContainers(containers) {
<div class="ops-line container-line" data-container="${container.name}">
<strong>${container.name}</strong>
<span>${container.status}</span>
<button class="ghost-button mini restart-container ${dangerActive && !container.restartable ? 'is-danger-enabled' : ''}" type="button" title="${container.restartable ? 'Container neu starten' : (dangerActive ? 'Danger-Modus aktiv: Neustart erlaubt' : (container.restartBlockReason || 'Neustart gesperrt'))}" ${(container.restartable || dangerActive) ? '' : 'disabled'}>
Neustarten
</button>
<div style="display: flex; gap: 6px;">
<button class="ghost-button mini view-container-logs" type="button">Logs</button>
<button class="ghost-button mini restart-container ${dangerActive && !container.restartable ? 'is-danger-enabled' : ''}" type="button" title="${container.restartable ? 'Container neu starten' : (dangerActive ? 'Danger-Modus aktiv: Neustart erlaubt' : (container.restartBlockReason || 'Neustart gesperrt'))}" ${(container.restartable || dangerActive) ? '' : 'disabled'}>
Neustarten
</button>
</div>
</div>
`).join('');
containerStatus.querySelectorAll('.view-container-logs').forEach((button) => {
button.addEventListener('click', () => {
openLogsModal(button.closest('.container-line').dataset.container);
});
});
containerStatus.querySelectorAll('.restart-container').forEach((button) => {
button.addEventListener('click', () => restartContainer(button.closest('.container-line')));
});
@@ -786,6 +808,262 @@ function startOpsAutoRefresh() {
}, 60000);
}
let currentLogsContainer = '';
async function fetchContainerLogs(containerName, tail = 150) {
const consoleEl = document.getElementById('logsModalConsole');
if (!consoleEl) return;
consoleEl.textContent = 'Lade Logs...';
try {
const response = await fetch(`/api/containers/${encodeURIComponent(containerName)}/logs?tail=${tail}`, {
headers: {
Authorization: `Bearer ${tokenSet?.id_token}`
}
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
if (typeof data.logs === 'string') {
consoleEl.textContent = data.logs || 'Keine Logs vorhanden.';
} else {
consoleEl.textContent = JSON.stringify(data.logs, null, 2);
}
consoleEl.scrollTop = consoleEl.scrollHeight;
} catch (error) {
consoleEl.textContent = `Fehler beim Laden der Logs: ${error.message}`;
}
}
function openLogsModal(containerName) {
currentLogsContainer = containerName;
const overlay = document.getElementById('logsModalOverlay');
const title = document.getElementById('logsModalTitle');
const tailSelect = document.getElementById('logsTailSelect');
if (overlay && title) {
title.textContent = `Logs von "${containerName}"`;
if (tailSelect) {
tailSelect.value = "150";
}
overlay.removeAttribute('hidden');
overlay.classList.add('is-active');
fetchContainerLogs(containerName, 150);
}
}
function closeLogsModal() {
const overlay = document.getElementById('logsModalOverlay');
if (overlay) {
overlay.setAttribute('hidden', '');
overlay.classList.remove('is-active');
currentLogsContainer = '';
}
}
async function openServiceInspectModal(url, name) {
const overlay = document.getElementById('serviceModalOverlay');
const title = document.getElementById('serviceModalTitle');
const content = document.getElementById('serviceModalContent');
if (!overlay || !content) return;
title.textContent = `Inspektion: ${name}`;
content.innerHTML = '<div class="ops-line">Führe Live-Diagnose aus...</div>';
overlay.removeAttribute('hidden');
overlay.classList.add('is-active');
try {
const response = await fetch(`/api/services/inspect?url=${encodeURIComponent(url)}`, {
headers: {
Authorization: `Bearer ${tokenSet?.id_token}`
}
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
const statusClass = data.ok ? 'text-ok' : 'text-down';
let headersHtml = '';
if (data.headers) {
headersHtml = Object.entries(data.headers).map(([key, val]) => `
<div class="header-row" style="display: flex; justify-content: space-between; font-size: 0.8rem; border-bottom: 1px solid var(--surface-2); padding: 4px 0;">
<strong style="color: var(--dim); word-break: break-all;">${key}:</strong>
<span style="color: #fff; text-align: right; word-break: break-all; margin-left: 8px;">${val}</span>
</div>
`).join('');
}
let previewHtml = '';
if (data.bodyPreview) {
const escaped = data.bodyPreview
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
previewHtml = `
<div style="margin-top: 14px;">
<h4 style="margin-bottom: 6px; font-size: 0.9rem;">Antwort-Vorschau (erste 1000 Zeichen):</h4>
<pre class="terminal-logs" style="max-height: 150px; font-size: 0.8rem; padding: 10px; background: rgba(0,0,0,0.3); border-radius: var(--radius); overflow-y: auto;">${escaped}</pre>
</div>
`;
}
content.innerHTML = `
<div class="ops-line">
<strong>Status:</strong>
<span class="${statusClass}" style="font-weight: 700; color: ${data.ok ? 'var(--ok)' : 'var(--down)'}">${data.statusCode ? `HTTP ${data.statusCode} ${data.statusMessage || ''}` : 'Fehler'}</span>
</div>
<div class="ops-line">
<strong>Latenz:</strong>
<span>${data.latency} ms</span>
</div>
${data.error ? `<div class="ops-line is-warning"><strong>Fehlerursache:</strong><span>${data.error}</span></div>` : ''}
<div style="margin-top: 14px;">
<h4 style="margin-bottom: 6px; font-size: 0.9rem;">HTTP Header:</h4>
<div class="headers-grid" style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.15); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px;">
${headersHtml || '<div style="color: var(--dim); font-size: 0.8rem;">Keine Header geladen</div>'}
</div>
</div>
${previewHtml}
`;
} catch (error) {
content.innerHTML = `<div class="ops-line is-warning"><strong>Fehler bei Diagnose:</strong><span>${error.message}</span></div>`;
}
}
function closeServiceModal() {
const overlay = document.getElementById('serviceModalOverlay');
if (overlay) {
overlay.setAttribute('hidden', '');
overlay.classList.remove('is-active');
}
}
async function startFolderScan(dirPath) {
const resultsEl = document.getElementById('scannerResults');
const btn = document.getElementById('scannerStartBtn');
if (!resultsEl || !btn) return;
btn.disabled = true;
const originalText = btn.textContent;
btn.textContent = 'Scanne...';
resultsEl.removeAttribute('hidden');
resultsEl.innerHTML = '<div class="ops-line">Scanne Verzeichnis und berechne Größen. Bitte warten...</div>';
try {
const response = await fetch(`/api/disks/scan-folder?path=${encodeURIComponent(dirPath)}`, {
headers: {
Authorization: `Bearer ${tokenSet?.id_token}`
}
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
if (!data.children || data.children.length === 0) {
resultsEl.innerHTML = '<div class="ops-line">Keine Dateien oder Verzeichnisse gefunden oder Zugriff verweigert.</div>';
return;
}
resultsEl.innerHTML = data.children.map((child) => {
const sizeStr = formatBytes(child.size);
const icon = child.isDirectory ? '📁' : child.isLink ? '🔗' : '📄';
const nameColor = child.isDirectory ? '#67e8f9' : '#fff';
let clickAction = '';
if (child.isDirectory && !child.error) {
clickAction = `class="scanner-folder-link" data-path="${child.path}" style="cursor: pointer; color: ${nameColor}; text-decoration: underline;"`;
} else {
clickAction = `style="color: ${nameColor};"`;
}
const errorText = child.error ? ' <span style="color: var(--down); font-size: 0.75rem;">(Fehler beim Lesen)</span>' : '';
return `
<div class="ops-line">
<span ${clickAction}>${icon} ${child.name}${errorText}</span>
<span>${sizeStr}</span>
</div>
`;
}).join('');
resultsEl.querySelectorAll('.scanner-folder-link').forEach((link) => {
link.addEventListener('click', () => {
const nextPath = link.dataset.path;
document.getElementById('scannerPathInput').value = nextPath;
startFolderScan(nextPath);
});
});
} catch (error) {
resultsEl.innerHTML = `<div class="ops-line is-warning"><strong>Scan fehlgeschlagen:</strong><span>${error.message}</span></div>`;
} finally {
btn.disabled = false;
btn.textContent = originalText;
}
}
function initFolderScanner() {
const btn = document.getElementById('scannerStartBtn');
const input = document.getElementById('scannerPathInput');
if (btn && input) {
btn.addEventListener('click', () => {
const val = input.value.trim();
if (val) startFolderScan(val);
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const val = input.value.trim();
if (val) startFolderScan(val);
}
});
}
}
function initModals() {
const logsCloseBtn = document.getElementById('logsModalCloseBtn');
const logsOverlay = document.getElementById('logsModalOverlay');
const logsTailSelect = document.getElementById('logsTailSelect');
const logsRefreshBtn = document.getElementById('logsModalRefreshBtn');
if (logsCloseBtn) {
logsCloseBtn.addEventListener('click', closeLogsModal);
}
if (logsOverlay) {
logsOverlay.addEventListener('click', (e) => {
if (e.target === logsOverlay) closeLogsModal();
});
}
if (logsTailSelect) {
logsTailSelect.addEventListener('change', () => {
if (currentLogsContainer) {
fetchContainerLogs(currentLogsContainer, Number(logsTailSelect.value));
}
});
}
if (logsRefreshBtn) {
logsRefreshBtn.addEventListener('click', () => {
if (currentLogsContainer) {
const tail = logsTailSelect ? Number(logsTailSelect.value) : 150;
fetchContainerLogs(currentLogsContainer, tail);
}
});
}
const serviceCloseBtn = document.getElementById('serviceModalCloseBtn');
const serviceOverlay = document.getElementById('serviceModalOverlay');
if (serviceCloseBtn) {
serviceCloseBtn.addEventListener('click', closeServiceModal);
}
if (serviceOverlay) {
serviceOverlay.addEventListener('click', (e) => {
if (e.target === serviceOverlay) closeServiceModal();
});
}
}
function initCollapsibleCards() {
document.querySelectorAll('.ops-card-head').forEach((head) => {
const card = head.closest('.ops-card');
@@ -934,6 +1212,8 @@ async function boot() {
renderEditor();
initCollapsibleCards();
initTabNavigation();
initModals();
initFolderScanner();
refreshOps();
startOpsAutoRefresh();
}
@@ -976,6 +1256,12 @@ if (refreshServicesButton) {
refreshServicesButton.addEventListener('click', () => refreshOps());
}
runWatchtowerButton.addEventListener('click', runWatchtowerOnce);
const viewWatchtowerLogsBtn = document.getElementById('viewWatchtowerLogsBtn');
if (viewWatchtowerLogsBtn) {
viewWatchtowerLogsBtn.addEventListener('click', () => {
openLogsModal('watchtower');
});
}
dangerModeButton.addEventListener('click', unlockDangerMode);
boot().catch((error) => {