Files
felhom.eu/documentation/tests/campaign2-evidence-2026-07-07/evidence/T-PAGE-ALL/body__monitoring.html
T
admin 7714f1f6e4 docs(tests): Campaign 2 report + evidence — DR primitives, self-update, storage, faults, endurance
Full-system validation on demo 9201 (controller v0.102.0 / agent v0.73.0). Headline:
ONE genuine pilot-critical finding — the non-root agent cannot pct-restore the
standard Felhom guest because it has bind-mount mountpoints (mp8 /mnt/felhom-drives,
mp9 bootstrap) and pct restore refuses bind mounts unless root. This breaks BOTH
--selftest=restore-test AND the offsite DR bring-up (R1), linked to a missing
VM.Audit/VM.Allocate on the agent token (R2). The large raw FAIL count is dominated
by one harness bug (auth-session expiry mis-scoring P3-P9) plus D1-injection/soak-probe/
dryrun-journal/escrow-blob calibration issues; the product where actually exercised was
healthy with ZERO resource leaks over ~7h. Demo node healed to pre-campaign state
(auth off, notifications restored, no scratch leaks). No production code changed.

Evidence snapshot + README under campaign2-evidence-2026-07-07/ (cookie jars scrubbed).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-07 14:26:52 +02:00

1232 lines
60 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="hu">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rendszermonitor — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="stylesheet" href="/static/style.css">
<meta name="csrf-token" content="ee8f0976099202660a6865daf2fe0e15217659da12a012a5dfd9233ce4cb157c">
<script>
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
</svg>
<nav class="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg" alt="Felhom.eu" class="sidebar-logo">
<span class="customer-name">Demo Ügyfél</span>
</div>
<ul class="nav-links">
<li><a href="/" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li><a href="/storage" class=""><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely</a>
<ul class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li><a href="/backups" class=""><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés</a></li>
<li><a href="/monitoring" class="active"><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.102.0</span>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="alerts-container">
<div class="alert-banner alert-banner-error">
<span class="alert-icon"><svg class="ico"><use href="#i-triangle-alert"/></svg></span>
<span class="alert-message">A tárolókezelő ügynök nem elérhető.</span>
<a href="/settings" class="alert-link">Beállítások →</a>
</div>
</div>
<div class="page-header">
<h2>Rendszermonitor</h2>
</div>
<div class="monitor-card" id="host-health-card">
<div class="monitor-card-header">
<h3>Szerver állapota (gazdagép)</h3>
<span class="sysinfo-value" id="host-health-updated" style="font-size:.75rem;color:var(--text-3)"></span>
</div>
<div id="host-health-unavailable" class="monitoring-banner monitoring-banner-yellow" style="display:none">
A gazdagép metrikái jelenleg nem elérhetők.
</div>
<div id="host-health-body" style="display:none">
<div class="sysinfo-grid">
<div class="sysinfo-row">
<span class="sysinfo-label">CPU használat</span>
<span class="sysinfo-value" id="host-cpu"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Terhelés (load)</span>
<span class="sysinfo-value" id="host-load"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Memória</span>
<span class="sysinfo-value" id="host-mem"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">CPU hőmérséklet</span>
<span class="sysinfo-value" id="host-temp"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Üzemidő</span>
<span class="sysinfo-value" id="host-uptime"></span>
</div>
</div>
<h4 style="margin:1rem 0 .5rem">Tárolók kapacitása</h4>
<div class="storage-bars" id="host-storage-bars"></div>
</div>
</div>
<div class="monitor-card">
<h3>Rendszer áttekintés</h3>
<div class="sysinfo-grid">
<div class="sysinfo-row">
<span class="sysinfo-label">Gépnév</span>
<span class="sysinfo-value" id="sysinfo-hostname"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Operációs rendszer</span>
<span class="sysinfo-value" id="sysinfo-os"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Kernel</span>
<span class="sysinfo-value" id="sysinfo-kernel"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Processzor</span>
<span class="sysinfo-value" id="sysinfo-cpu"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Üzemidő</span>
<span class="sysinfo-value" id="sysinfo-uptime"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Indítás</span>
<span class="sysinfo-value" id="sysinfo-boot"></span>
</div>
</div>
</div>
<div class="monitor-card">
<h3>Tárhely</h3>
<div class="storage-bars">
<div class="storage-item meter nominal">
<div class="storage-header">
<span class="storage-label">Rendszer (/)</span>
<span class="storage-value">20.9 GB / 196 GB (11%)</span>
</div>
<div class="meter-track">
<div class="meter-fill" style="width:10.7%"></div>
</div>
</div>
<div class="storage-item meter nominal">
<div class="storage-header">
<span class="storage-label">Tárhely (felhom-flash)</span>
<span class="storage-value">4.02 GB / 117 GB (3%)</span>
</div>
<div class="meter-track">
<div class="meter-fill" style="width:3.4%"></div>
</div>
<div class="storage-purpose" style="font-size:.72rem;opacity:.65;margin-top:.2rem">Külső adattároló — a telepített alkalmazások nagy méretű fájljai (média, dokumentumok) ide kerülnek; az adatbázisok a belső SSD-n vannak.</div>
</div>
<div class="storage-item meter nominal">
<div class="storage-header">
<span class="storage-label">Tárhely (felhom-usb)</span>
<span class="storage-value">1.21 GB / 916 GB (0%)</span>
</div>
<div class="meter-track">
<div class="meter-fill" style="width:0.1%"></div>
</div>
<div class="storage-purpose" style="font-size:.72rem;opacity:.65;margin-top:.2rem">Külső adattároló — a telepített alkalmazások nagy méretű fájljai (média, dokumentumok) ide kerülnek; az adatbázisok a belső SSD-n vannak.</div>
</div>
<div class="storage-item meter nominal">
<div class="storage-header">
<span class="storage-label">teszt_enroll</span>
<span class="storage-value">0.00 GB / 58.2 GB (0%)</span>
</div>
<div class="meter-track">
<div class="meter-fill" style="width:0.0%"></div>
</div>
<div class="storage-purpose" style="font-size:.72rem;opacity:.65;margin-top:.2rem">Külső adattároló — a telepített alkalmazások nagy méretű fájljai (média, dokumentumok) ide kerülnek; az adatbázisok a belső SSD-n vannak.</div>
</div>
</div>
</div>
<div class="monitor-card">
<h3>Hub kapcsolat</h3>
<div class="monitoring-banner monitoring-banner-green">
Kapcsolódva — a központi rendszer aktívan figyeli a szervert.
</div>
<div class="sysinfo-grid" style="margin-top: 0.75rem">
<div class="sysinfo-row">
<span class="sysinfo-label">Hub URL</span>
<span class="sysinfo-value"><code>https://hub.felhom.eu</code></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Ügyfél azonosító</span>
<span class="sysinfo-value"><code>demo-felhom</code></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Utolsó sikeres jelentés</span>
<span class="sysinfo-value">4 perce</span>
</div>
</div>
</div>
<div class="monitor-card">
<div class="monitor-card-header">
<h3>Rendszer metrikák</h3>
<div class="time-range-bar" id="system-range-bar">
<button class="filter-btn active" data-range="1h">1 óra</button>
<button class="filter-btn" data-range="6h">6 óra</button>
<button class="filter-btn" data-range="24h">24 óra</button>
<button class="filter-btn" data-range="7d">7 nap</button>
<button class="filter-btn" data-range="30d">30 nap</button>
</div>
</div>
<div class="charts-grid" id="system-charts">
<div class="chart-box">
<div class="chart-title">CPU használat (%)</div>
<div class="chart-wrap"><canvas id="chart-cpu"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">Memória használat (GB)</div>
<div class="chart-wrap"><canvas id="chart-memory"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">Hőmérséklet (°C)</div>
<div class="chart-wrap"><canvas id="chart-temp"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">Terhelés (Load Average)</div>
<div class="chart-wrap"><canvas id="chart-load"></canvas></div>
</div>
</div>
<div class="chart-empty" id="system-charts-empty" style="display:none">
Még nincsenek adatok. A metrikák gyűjtése elindult, az első adatok néhány perc múlva megjelennek.
</div>
</div>
<div class="monitor-card" id="memory-distribution-card" style="display:none">
<h3>Memória eloszlás</h3>
<div id="mem-dist-header" class="memory-dist-header"></div>
<div class="memory-dist-bar" id="mem-dist-bar"></div>
<div class="memory-bar-legend" id="mem-dist-legend"></div>
</div>
<div class="monitor-card">
<h3>Alkalmazás erőforrások</h3>
<div class="container-charts-row" id="container-charts">
<div class="chart-box chart-box-half">
<div class="chart-title">CPU használat (%)</div>
<div class="chart-wrap chart-wrap-bar"><canvas id="chart-container-cpu"></canvas></div>
</div>
<div class="chart-box chart-box-half">
<div class="chart-title">Memória használat (MB)</div>
<div class="chart-wrap chart-wrap-bar"><canvas id="chart-container-mem"></canvas></div>
</div>
</div>
<div class="chart-empty" id="container-charts-empty" style="display:none">
Még nincsenek konténer adatok.
</div>
</div>
<div class="monitor-card" id="container-detail-panel" style="display:none">
<div class="monitor-card-header">
<h3 id="container-detail-title"></h3>
<div class="time-range-bar" id="container-range-bar">
<button class="filter-btn active" data-range="1h">1 óra</button>
<button class="filter-btn" data-range="6h">6 óra</button>
<button class="filter-btn" data-range="24h">24 óra</button>
<button class="filter-btn" data-range="7d">7 nap</button>
</div>
<button class="btn btn-outline btn-sm" onclick="closeContainerDetail()">Bezárás</button>
</div>
<div class="charts-grid charts-grid-2">
<div class="chart-box">
<div class="chart-title">CPU %</div>
<div class="chart-wrap"><canvas id="chart-detail-cpu"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">Memória (MB)</div>
<div class="chart-wrap"><canvas id="chart-detail-mem"></canvas></div>
</div>
</div>
</div>
<script src="/static/chart.min.js"></script>
<script>
(function() {
'use strict';
var colors = {
cpu: {border: '#2EA8F5', bg: 'rgba(46,168,245,0.1)'},
memory: {border: '#8E7CE8', bg: 'rgba(142,124,232,0.1)'},
temp: {border: '#E0A93E', bg: 'rgba(224,169,62,0.1)'},
load: {border: '#5EC4B6', bg: 'rgba(94,196,182,0.1)'}
};
function parseRangeMs(range) {
switch (range) {
case '1h': return 3600000;
case '6h': return 21600000;
case '24h': return 86400000;
case '7d': return 604800000;
case '30d': return 2592000000;
default: return 3600000;
}
}
function tickFormatForRange(range) {
if (range === '7d' || range === '30d') return 'date';
return 'time';
}
function chartOpts(yLabel, beginAtZero) {
var now = Date.now();
var defaultRangeMs = parseRangeMs('1h');
return {
responsive: true,
maintainAspectRatio: false,
animation: {duration: 300},
plugins: {
legend: {display: false},
tooltip: {
backgroundColor: '#0F1B2E',
titleColor: '#EDF2F9',
bodyColor: '#94A6BF',
borderColor: '#22344F',
borderWidth: 1,
callbacks: {
title: function(items) {
if (!items.length) return '';
var raw = items[0].raw;
if (raw && typeof raw === 'object' && raw.x) {
return formatTimestamp(raw.x);
}
if (items[0].parsed && items[0].parsed.x) {
return formatTimestamp(items[0].parsed.x);
}
return '';
}
}
}
},
scales: {
x: {
type: 'linear',
min: now - defaultRangeMs,
max: now,
grid: {color: 'rgba(34,52,79,0.5)'},
ticks: {
color: '#94A6BF',
maxTicksLimit: 8,
callback: function(v) {
return formatTimeLabel(v);
}
}
},
y: {
grid: {color: 'rgba(34,52,79,0.5)'},
ticks: {color: '#94A6BF'},
beginAtZero: beginAtZero !== false,
title: {display: !!yLabel, text: yLabel || '', color: '#5E7392', font: {size: 11}}
}
}
};
}
var lineDataset = function(color) {
return {
borderColor: color.border,
backgroundColor: color.bg,
borderWidth: 2,
pointRadius: 0,
pointHitRadius: 10,
tension: 0.3,
fill: true,
spanGaps: true
};
};
var budaTZ = 'Europe/Budapest';
var currentTickFormat = 'time';
function formatTimestamp(ts) {
if (ts === null || ts === undefined || ts === '') return '';
if (typeof ts === 'string') ts = Number(ts);
if (isNaN(ts)) return '';
var d = new Date(ts);
return d.toLocaleString('hu-HU', {timeZone: budaTZ, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'});
}
function formatTimeLabel(ts) {
if (ts === null || ts === undefined || ts === '') return '';
if (typeof ts === 'string') ts = Number(ts);
if (isNaN(ts)) return '';
var d = new Date(ts);
if (currentTickFormat === 'date') {
return d.toLocaleDateString('hu-HU', {timeZone: budaTZ, month: '2-digit', day: '2-digit'});
}
return d.toLocaleTimeString('hu-HU', {timeZone: budaTZ, hour: '2-digit', minute: '2-digit'});
}
function buildXYData(timestamps, values) {
var points = [];
for (var i = 0; i < timestamps.length; i++) {
points.push({x: timestamps[i], y: values[i]});
}
return points;
}
function setChartXBounds(chart, range) {
var now = Date.now();
var rangeMs = parseRangeMs(range);
chart.options.scales.x.min = now - rangeMs;
chart.options.scales.x.max = now;
}
function updateLineChart(chart, timestamps, values) {
chart.data.datasets[0].data = buildXYData(timestamps, values);
chart.update('none');
}
var systemRange = '1h';
var chartCPU, chartMem, chartTemp, chartLoad;
function initSystemCharts() {
var mkChart = function(id, color, yLabel, beginAtZero) {
return new Chart(document.getElementById(id), {
type: 'line',
data: {datasets: [{data: [], ...lineDataset(color)}]},
options: chartOpts(yLabel, beginAtZero)
});
};
chartCPU = mkChart('chart-cpu', colors.cpu, '%', true);
chartMem = mkChart('chart-memory', colors.memory, 'GB', true);
chartTemp = mkChart('chart-temp', colors.temp, '°C', false);
chartLoad = mkChart('chart-load', colors.load, '', true);
}
async function loadSystemMetrics() {
try {
var resp = await fetch('/api/metrics/system?range=' + systemRange + '&resolution=200');
var json = await resp.json();
if (!json.ok || !json.data) return;
var d = json.data;
if (!d.labels || d.labels.length === 0) {
document.getElementById('system-charts').style.display = 'none';
document.getElementById('system-charts-empty').style.display = 'block';
return;
}
document.getElementById('system-charts').style.display = '';
document.getElementById('system-charts-empty').style.display = 'none';
var timestamps = d.labels.map(function(ts) { return ts * 1000; });
currentTickFormat = tickFormatForRange(systemRange);
var allCharts = [chartCPU, chartMem, chartTemp, chartLoad];
allCharts.forEach(function(c) { setChartXBounds(c, systemRange); });
updateLineChart(chartCPU, timestamps, d.cpu);
updateLineChart(chartMem, timestamps, d.memory);
updateLineChart(chartTemp, timestamps, d.temp);
updateLineChart(chartLoad, timestamps, d.load1);
} catch(e) {
console.error('Failed to load system metrics:', e);
}
}
document.getElementById('system-range-bar').addEventListener('click', function(e) {
var btn = e.target.closest('.filter-btn');
if (!btn) return;
this.querySelectorAll('.filter-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
systemRange = btn.dataset.range;
loadSystemMetrics();
});
var chartContainerCPU, chartContainerMem;
var containerNames = [];
function initContainerCharts() {
var barOpts = function(xLabel) {
return {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
animation: {duration: 300},
plugins: {
legend: {display: false},
tooltip: {
backgroundColor: '#0F1B2E',
titleColor: '#EDF2F9',
bodyColor: '#94A6BF',
borderColor: '#22344F',
borderWidth: 1
}
},
scales: {
x: {
grid: {color: 'rgba(34,52,79,0.5)'},
ticks: {color: '#94A6BF'},
beginAtZero: true,
title: {display: true, text: xLabel, color: '#5E7392', font: {size: 11}}
},
y: {
grid: {display: false},
ticks: {color: '#94A6BF', font: {size: 12}}
}
},
onClick: function(evt, elements) {
if (elements.length > 0) {
var idx = elements[0].index;
if (containerNames[idx]) {
showContainerDetail(containerNames[idx]);
}
}
}
};
};
chartContainerCPU = new Chart(document.getElementById('chart-container-cpu'), {
type: 'bar',
data: {labels: [], datasets: [{data: [], backgroundColor: '#2EA8F5', borderRadius: 2}]},
options: barOpts('%')
});
chartContainerMem = new Chart(document.getElementById('chart-container-mem'), {
type: 'bar',
data: {labels: [], datasets: [{data: [], backgroundColor: '#8E7CE8', borderRadius: 2}]},
options: barOpts('MB')
});
}
async function loadContainerSummary() {
try {
var resp = await fetch('/api/metrics/containers/summary');
var json = await resp.json();
if (!json.ok || !json.data) return;
var data = json.data;
if (!data.length) {
document.getElementById('container-charts').style.display = 'none';
document.getElementById('container-charts-empty').style.display = 'block';
return;
}
document.getElementById('container-charts').style.display = '';
document.getElementById('container-charts-empty').style.display = 'none';
containerNames = data.map(function(c) { return c.name; });
var cpuData = data.map(function(c) { return Math.round(c.cpu_percent * 100) / 100; });
var memData = data.map(function(c) { return Math.round(c.mem_usage_mb); });
var h = Math.max(200, data.length * 35 + 60);
document.querySelectorAll('.chart-wrap-bar').forEach(function(el) { el.style.height = h + 'px'; });
chartContainerCPU.data.labels = containerNames;
chartContainerCPU.data.datasets[0].data = cpuData;
chartContainerCPU.update('none');
chartContainerMem.data.labels = containerNames;
chartContainerMem.data.datasets[0].data = memData;
chartContainerMem.update('none');
buildMemoryDistributionBar(data);
} catch(e) {
console.error('Failed to load container summary:', e);
}
}
var memDistPalette = ['#2EA8F5','#8E7CE8','#5EC4B6','#E0A93E','#D67BB5','#7FA6E8','#59B88C','#E8935E','#6FCADF','#B3A1F0'];
async function buildMemoryDistributionBar(containers) {
var totalMB = 12288 ;
if (!totalMB) return;
var usedMB = 0;
try {
var resp = await fetch('/api/system/info');
var json = await resp.json();
if (json.ok && json.data && json.data.system) usedMB = json.data.system.used_mem_mb || 0;
} catch(e) {}
if (!usedMB) return;
var card = document.getElementById('memory-distribution-card');
var bar = document.getElementById('mem-dist-bar');
var legend = document.getElementById('mem-dist-legend');
var header = document.getElementById('mem-dist-header');
var appTotal = 0;
containers.forEach(function(c) { appTotal += c.mem_usage_mb || 0; });
var osMB = Math.max(0, usedMB - appTotal);
var freeMB = Math.max(0, totalMB - usedMB);
function fmtMB(mb) { return mb >= 1024 ? (mb/1024).toFixed(1) + ' GB' : Math.round(mb) + ' MB'; }
header.textContent = 'Használt: ' + fmtMB(usedMB) + ' / ' + fmtMB(totalMB) + ' (' + Math.round(usedMB/totalMB*100) + '%)';
containers.sort(function(a, b) { return (b.mem_usage_mb || 0) - (a.mem_usage_mb || 0); });
var html = '';
var legendHtml = '';
containers.forEach(function(c, i) {
var mb = c.mem_usage_mb || 0;
if (mb < 1) return;
var pct = (mb / totalMB * 100).toFixed(2);
var color = memDistPalette[i % memDistPalette.length];
html += '<div class="memory-bar-segment" style="width:' + pct + '%;background:' + color + '" title="' + c.name + ': ' + fmtMB(mb) + '"></div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:' + color + '"></span>' + c.name + ' (' + fmtMB(mb) + ')</div>';
});
if (osMB > 10) {
var osPct = (osMB / totalMB * 100).toFixed(2);
html += '<div class="memory-bar-segment" style="width:' + osPct + '%;background:var(--text-3);opacity:0.5" title="Rendszer: ' + fmtMB(osMB) + '"></div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--text-3);opacity:0.5"></span>Rendszer (' + fmtMB(osMB) + ')</div>';
}
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--bg-2);border:1px solid var(--line)"></span>Szabad (' + fmtMB(freeMB) + ')</div>';
bar.innerHTML = html;
legend.innerHTML = legendHtml;
card.style.display = '';
}
var detailChartCPU, detailChartMem;
var detailContainer = '';
var detailRange = '1h';
function initDetailCharts() {
var mkChart = function(id, color, yLabel) {
return new Chart(document.getElementById(id), {
type: 'line',
data: {datasets: [{data: [], ...lineDataset(color)}]},
options: chartOpts(yLabel, true)
});
};
detailChartCPU = mkChart('chart-detail-cpu', colors.cpu, '%');
detailChartMem = mkChart('chart-detail-mem', colors.memory, 'MB');
}
window.showContainerDetail = async function(name) {
detailContainer = name;
document.getElementById('container-detail-title').textContent = name + ' — Erőforrás előzmények';
document.getElementById('container-detail-panel').style.display = '';
document.getElementById('container-detail-panel').scrollIntoView({behavior: 'smooth'});
await loadContainerDetail();
};
window.closeContainerDetail = function() {
document.getElementById('container-detail-panel').style.display = 'none';
detailContainer = '';
};
async function loadContainerDetail() {
if (!detailContainer) return;
try {
var resp = await fetch('/api/metrics/containers/' + encodeURIComponent(detailContainer) + '?range=' + detailRange + '&resolution=150');
var json = await resp.json();
if (!json.ok || !json.data) return;
var d = json.data;
var timestamps = (d.labels || []).map(function(ts) { return ts * 1000; });
setChartXBounds(detailChartCPU, detailRange);
setChartXBounds(detailChartMem, detailRange);
updateLineChart(detailChartCPU, timestamps, d.cpu || []);
updateLineChart(detailChartMem, timestamps, d.memory || []);
} catch(e) {
console.error('Failed to load container detail:', e);
}
}
document.getElementById('container-range-bar').addEventListener('click', function(e) {
var btn = e.target.closest('.filter-btn');
if (!btn) return;
this.querySelectorAll('.filter-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
detailRange = btn.dataset.range;
loadContainerDetail();
});
function usageColorClass(pct) {
if (pct >= 85) return 'crit';
if (pct >= 70) return 'warn';
return 'nominal';
}
function fmtBytesGB(bytes) {
if (!bytes || bytes <= 0) return '0 GB';
var gb = bytes / 1073741824;
if (gb >= 1024) return (gb / 1024).toFixed(2) + ' TB';
if (gb >= 100) return Math.round(gb) + ' GB';
return gb.toFixed(1) + ' GB';
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function(c) {
return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];
});
}
function showHostUnavailable() {
document.getElementById('host-health-unavailable').style.display = '';
document.getElementById('host-health-body').style.display = 'none';
document.getElementById('host-health-updated').textContent = '';
}
function renderHostHealth(d) {
var h = d.host || {};
document.getElementById('host-health-unavailable').style.display = 'none';
document.getElementById('host-health-body').style.display = '';
document.getElementById('host-cpu').textContent =
(h.cpu_percent != null ? h.cpu_percent.toFixed(1) : '') + '%';
var load = Array.isArray(h.loadavg) ? h.loadavg.join(' ') : '';
document.getElementById('host-load').textContent = load || '';
if (h.memory_total_bytes > 0) {
document.getElementById('host-mem').textContent =
fmtBytesGB(h.memory_used_bytes) + ' / ' + fmtBytesGB(h.memory_total_bytes) +
' (' + Math.round(h.memory_percent || 0) + '%)';
} else {
document.getElementById('host-mem').textContent = '';
}
document.getElementById('host-temp').textContent =
(h.cpu_temp_c == null) ? 'n/a' : (h.cpu_temp_c + ' °C');
document.getElementById('host-uptime').textContent =
(h.uptime_seconds > 0) ? formatUptime(h.uptime_seconds) : '';
var bars = document.getElementById('host-storage-bars');
var targets = Array.isArray(d.storage_targets) ? d.storage_targets : [];
if (!targets.length) {
bars.innerHTML = '<div class="sysinfo-value" style="color:var(--text-3)">Nincs tároló adat.</div>';
} else {
var html = '';
targets.forEach(function(t) {
var friendly = escapeHtml(t.label || t.name || '');
var rawId = escapeHtml(t.name || '');
var idHtml = rawId ? ' <span style="color:var(--text-3);font-size:.72rem">(' + rawId + ')</span>' : '';
var label = friendly + idHtml;
var purposeHtml = t.purpose ? '<div class="storage-purpose" style="font-size:.72rem;opacity:.65;margin-top:.2rem">' + escapeHtml(t.purpose) + '</div>' : '';
if (t.state && t.state !== 'attached') {
html += '<div class="storage-item meter meter-empty storage-disconnected">' +
'<div class="storage-header"><span class="storage-label">' + label + '</span>' +
'<span class="storage-value">— <span class="state-text-neutral">Nem elérhető</span></span></div>' +
'<div class="meter-track"></div>' + purposeHtml + '</div>';
return;
}
var pct = (t.used_fraction != null ? t.used_fraction * 100 : 0);
var extra = [];
if (t.thin_pool && t.thin_pool.data_used_fraction != null) {
extra.push('thin-pool: ' + Math.round(t.thin_pool.data_used_fraction * 100) + '%');
}
if (t.smart) {
if (t.smart.temperature_c != null) extra.push(t.smart.temperature_c + ' °C');
if (t.smart.percentage_used != null) extra.push('kopás: ' + t.smart.percentage_used + '%');
}
var extraHtml = extra.length ? ' <span style="color:var(--text-3);font-size:.75rem">· ' + escapeHtml(extra.join(' · ')) + '</span>' : '';
html += '<div class="storage-item meter ' + usageColorClass(pct) + '">' +
'<div class="storage-header">' +
'<span class="storage-label">' + label + extraHtml + '</span>' +
'<span class="storage-value">' + fmtBytesGB(t.used_bytes) + ' / ' + fmtBytesGB(t.total_bytes) +
' (' + Math.round(pct) + '%)</span></div>' +
'<div class="meter-track"><div class="meter-fill' +
'" style="width:' + Math.min(100, pct).toFixed(1) + '%"></div></div>' + purposeHtml + '</div>';
});
bars.innerHTML = html;
}
var now = new Date();
document.getElementById('host-health-updated').textContent =
'Frissítve: ' + now.toLocaleTimeString('hu-HU', {timeZone: budaTZ, hour: '2-digit', minute: '2-digit', second: '2-digit'});
}
async function loadHostMetrics() {
try {
var resp = await fetch('/api/host-metrics');
var json = await resp.json();
if (!json.ok || !json.data) { showHostUnavailable(); return; }
renderHostHealth(json.data);
} catch(e) {
console.error('Failed to load host metrics:', e);
showHostUnavailable();
}
}
async function loadSysInfo() {
try {
var resp = await fetch('/api/metrics/sysinfo');
var json = await resp.json();
if (!json.ok || !json.data) return;
var d = json.data;
document.getElementById('sysinfo-hostname').textContent = d.hostname || '';
document.getElementById('sysinfo-os').textContent = d.os || '';
document.getElementById('sysinfo-kernel').textContent = d.kernel || '';
var cpuText = d.cpu_model || '';
if (d.cpu_cores > 0) cpuText += ' (' + d.cpu_cores + ' mag)';
document.getElementById('sysinfo-cpu').textContent = cpuText;
if (d.uptime_seconds > 0) {
document.getElementById('sysinfo-uptime').textContent = formatUptime(d.uptime_seconds);
}
if (d.boot_time) {
var bt = new Date(d.boot_time);
document.getElementById('sysinfo-boot').textContent = bt.toLocaleString('hu-HU', {timeZone: budaTZ, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'});
}
} catch(e) {
console.error('Failed to load sysinfo:', e);
}
}
function formatUptime(seconds) {
var days = Math.floor(seconds / 86400);
var hours = Math.floor((seconds % 86400) / 3600);
var minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) return days + ' nap, ' + hours + ' óra';
if (hours > 0) return hours + ' óra, ' + minutes + ' perc';
return minutes + ' perc';
}
initSystemCharts();
initContainerCharts();
initDetailCharts();
loadSysInfo();
loadSystemMetrics();
loadContainerSummary();
loadHostMetrics();
setInterval(function() {
loadSystemMetrics();
loadContainerSummary();
if (detailContainer) loadContainerDetail();
}, 60000);
setInterval(loadHostMetrics, 8000);
})();
</script>
</main>
<script>
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .stack-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíti az alkalmazást, az adatokat újra importálnia kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>