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
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
This commit is contained in:
294
admin.js
294
admin.js
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
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) => {
|
||||
|
||||
Reference in New Issue
Block a user