feat(dashboard): tab Infrastructura, panouri colapsabile si compacte

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KddsXCqEbKMhdFJDYbAsx8
This commit is contained in:
Claude Agent
2026-09-13 10:18:12 +00:00
parent dc61c43b32
commit 56027e2e6c
4 changed files with 761 additions and 134 deletions

View File

@@ -11,178 +11,358 @@
<header class="header">
<span class="logo"><span class="dot" id="dot"></span> Punte Discord</span>
<nav class="tabs" aria-label="Secțiuni" id="tabNav">
<button class="tab" type="button" data-tab="punte">Punte</button>
<button class="tab" type="button" data-tab="maria">Maria</button>
<button class="tab" type="button" data-tab="infra">Infrastructură</button>
</nav>
<span class="spacer"></span>
<span class="meta" id="lastPoll">—</span>
<button class="small" id="btnToggleAll" onclick="toggleAllPanels()">Restrânge tot</button>
<button class="small" onclick="toggleTheme()" title="Schimbă tema">◐</button>
<button class="small" id="btnLogout" onclick="logout()" hidden>Ieși</button>
</header>
<div class="wrap">
<!-- ══════════════════════════ PUNTE ══════════════════════════ -->
<div class="wrap tabpage" data-tab="punte">
<!-- ── serviciu ───────────────────────────────────────────── -->
<section class="card">
<h2>Serviciu <span class="count" id="unitName"></span></h2>
<div class="metrics" id="metrics"></div>
<div class="actions" style="margin-top:var(--space-5)">
<button class="primary" id="btnRestart" onclick="svc('restart')">Repornește</button>
<button id="btnStart" onclick="svc('start')">Pornește</button>
<button class="danger" id="btnStop" onclick="svc('stop')">Oprește</button>
<span class="spacer"></span>
<button class="small" onclick="cleanup(true)">Caută orfani</button>
<button class="small danger" onclick="cleanup(false)">Curăță orfani</button>
<button class="small" onclick="restartSelf()">Repornește dashboard-ul</button>
<details class="card" open data-panel="serviciu">
<summary><span class="chev">▶</span><span class="ptitle">Serviciu</span><span class="count" id="unitName"></span></summary>
<div class="pbody">
<div class="metrics" id="metrics"></div>
<div class="actions">
<button class="primary" id="btnRestart" onclick="svc('restart')">Repornește</button>
<button id="btnStart" onclick="svc('start')">Pornește</button>
<button class="danger" id="btnStop" onclick="svc('stop')">Oprește</button>
<span class="spacer"></span>
<button class="small" onclick="cleanup(true)">Caută orfani</button>
<button class="small danger" onclick="cleanup(false)">Curăță orfani</button>
<button class="small" onclick="restartSelf()">Repornește dashboard-ul</button>
</div>
</div>
</section>
</details>
<div class="grid-2">
<!-- ── fire ─────────────────────────────────────────────── -->
<section class="card">
<h2>Fire active <span class="count" id="threadCount"></span></h2>
<div class="table-scroll">
<table class="stack-mobil">
<thead><tr><th>Fir</th><th>Model</th><th>Director</th><th>Stare</th><th class="num">USD</th></tr></thead>
<tbody id="threads"></tbody>
</table>
<details class="card" open data-panel="fire">
<summary><span class="chev">▶</span><span class="ptitle">Fire active</span><span class="count" id="threadCount"></span></summary>
<div class="pbody">
<div class="table-scroll">
<table class="stack-mobil">
<thead><tr><th>Fir</th><th>Model</th><th>Director</th><th>Stare</th><th class="num">USD</th></tr></thead>
<tbody id="threads"></tbody>
</table>
</div>
</div>
</section>
</details>
<!-- ── diagnostic ───────────────────────────────────────── -->
<section class="card">
<h2>Diagnostic</h2>
<div id="doctor"><div class="empty">se încarcă…</div></div>
</section>
<details class="card" open data-panel="diagnostic">
<summary><span class="chev">▶</span><span class="ptitle">Diagnostic</span></summary>
<div class="pbody">
<div id="doctor"><div class="empty">se încarcă…</div></div>
</div>
</details>
</div>
<!-- ── aprobări ───────────────────────────────────────────── -->
<section class="card">
<h2>Confirmări în așteptare <span class="count" id="apprCount"></span></h2>
<div id="approvals"><div class="empty">niciuna</div></div>
</section>
<details class="card" open data-panel="aprobari">
<summary><span class="chev">▶</span><span class="ptitle">Confirmări în așteptare</span><span class="count" id="apprCount"></span></summary>
<div class="pbody">
<div id="approvals"><div class="empty">niciuna</div></div>
</div>
</details>
<!-- ── jurnal ─────────────────────────────────────────────── -->
<section class="card">
<h2>Jurnal
<details class="card" open data-panel="jurnal-bot">
<summary><span class="chev">▶</span><span class="ptitle">Jurnal</span>
<span class="count">
<button class="small" id="tabBot" onclick="setLog('bot')">bot.log</button>
<button class="small" id="tabInfra" onclick="setLog('infra')">infra.log</button>
<button class="small" onclick="refreshLogs()">↻</button>
</span>
</h2>
<pre class="log" id="log">se încarcă…</pre>
</section>
</summary>
<div class="pbody">
<pre class="log" id="log">se încarcă…</pre>
</div>
</details>
<!-- ── Maria: WhatsApp + RAG ──────────────────────────────── -->
<h2 style="margin-top:var(--space-6)">Maria — WhatsApp + RAG</h2>
</div>
<!-- ══════════════════════════ MARIA ══════════════════════════ -->
<div class="wrap tabpage" data-tab="maria">
<div class="grid-2">
<section class="card">
<h2>Punte WhatsApp <span class="count" id="mBridgeUnit"></span></h2>
<div class="metrics" id="mBridgeMetrics"></div>
<div class="actions" style="margin-top:var(--space-5)">
<button onclick="mSvc('bridge','start')">Pornește</button>
<button class="danger" onclick="mSvc('bridge','stop')">Oprește</button>
<button class="primary" onclick="mSvc('bridge','restart')">Repornește</button>
<details class="card" open data-panel="maria-bridge">
<summary><span class="chev">▶</span><span class="ptitle">Punte WhatsApp</span><span class="count" id="mBridgeUnit"></span></summary>
<div class="pbody">
<div class="metrics" id="mBridgeMetrics"></div>
<div class="actions">
<button onclick="mSvc('bridge','start')">Pornește</button>
<button class="danger" onclick="mSvc('bridge','stop')">Oprește</button>
<button class="primary" onclick="mSvc('bridge','restart')">Repornește</button>
</div>
</div>
</section>
<section class="card">
<h2>Consumer RAG <span class="count" id="mRagUnit"></span></h2>
<div class="metrics" id="mRagMetrics"></div>
<div class="actions" style="margin-top:var(--space-5)">
<button onclick="mSvc('rag','start')">Pornește</button>
<button class="danger" onclick="mSvc('rag','stop')">Oprește</button>
<button class="primary" onclick="mSvc('rag','restart')">Repornește</button>
</details>
<details class="card" open data-panel="maria-rag">
<summary><span class="chev">▶</span><span class="ptitle">Consumer RAG</span><span class="count" id="mRagUnit"></span></summary>
<div class="pbody">
<div class="metrics" id="mRagMetrics"></div>
<div class="actions">
<button onclick="mSvc('rag','start')">Pornește</button>
<button class="danger" onclick="mSvc('rag','stop')">Oprește</button>
<button class="primary" onclick="mSvc('rag','restart')">Repornește</button>
</div>
</div>
</section>
</details>
</div>
<section class="card">
<h2>Conectare WhatsApp</h2>
<div id="mWaStatus" class="empty">se încarcă…</div>
<div id="mWaQr"></div>
<div id="mWaPair" hidden>
<div class="detail" style="margin-top:var(--space-5)">
Nu poți scana codul QR? Cere un <strong>cod de asociere</strong> și
introdu-l pe telefon în WhatsApp → Dispozitive conectate → Conectează un
dispozitiv → <em>Conectează cu număr de telefon</em>.
<details class="card" open data-panel="maria-wa">
<summary><span class="chev">▶</span><span class="ptitle">Conectare WhatsApp</span></summary>
<div class="pbody">
<div id="mWaStatus" class="empty">se încarcă…</div>
<div id="mWaQr"></div>
<div id="mWaPair" hidden>
<div class="detail">
Nu poți scana codul QR? Cere un <strong>cod de asociere</strong> și
introdu-l pe telefon în WhatsApp → Dispozitive conectate → Conectează un
dispozitiv → <em>Conectează cu număr de telefon</em>.
</div>
<div class="actions">
<input type="text" id="mPairPhone" placeholder="40723197939 (internațional, fără +)"
style="flex:1;min-width:220px">
<button onclick="mPair()">Cere cod de asociere</button>
</div>
<div id="mPairOut" class="detail"></div>
</div>
</div>
</details>
<details class="card" open data-panel="maria-esc">
<summary><span class="chev">▶</span><span class="ptitle">Trimise la suport</span><span class="count" id="mEscCount"></span></summary>
<div class="pbody">
<div class="detail" id="mSupportDetail"></div>
<div id="mEsc" class="empty">se încarcă…</div>
</div>
</details>
<details class="card" open data-panel="maria-fire">
<summary><span class="chev">▶</span><span class="ptitle">Fire de discuție active</span><span class="count" id="mFirCount"></span></summary>
<div class="pbody">
<div class="table-scroll">
<table class="stack-mobil">
<thead><tr><th>JID</th><th>Subiect</th><th>Ultima activitate</th><th></th></tr></thead>
<tbody id="mFire"></tbody>
</table>
</div>
</div>
</details>
<details class="card" data-panel="maria-clienti">
<summary><span class="chev">▶</span><span class="ptitle">Clienți cunoscuți</span><span class="count" id="mClientCount"></span></summary>
<div class="pbody">
<div class="table-scroll">
<table class="stack-mobil">
<thead><tr><th>Nume</th><th>Numere</th><th></th><th></th></tr></thead>
<tbody id="mClients"></tbody>
</table>
</div>
<h3 id="mClientFormTitle">Adaugă client</h3>
<input type="text" id="mClientId" placeholder="id (ex: acme)" style="width:100%;margin-bottom:var(--space-3)">
<input type="text" id="mClientNume" placeholder="nume firmă" style="width:100%;margin-bottom:var(--space-3)">
<textarea id="mClientProfil" placeholder="profil (context liber despre client)..." style="width:100%;min-height:80px;margin-bottom:var(--space-3)"></textarea>
<textarea id="mClientNumere" placeholder="numere de telefon, unul pe linie" style="width:100%;min-height:80px;font-family:monospace"></textarea>
<div class="actions" style="margin-top:var(--space-3)">
<input type="text" id="mPairPhone" placeholder="40723197939 (internațional, fără +)"
style="flex:1;min-width:220px">
<button onclick="mPair()">Cere cod de asociere</button>
<button class="primary" onclick="mSaveClient()">Salvează</button>
<button class="small" onclick="mResetClientForm()">Renunță</button>
</div>
<div id="mPairOut" class="detail" style="margin-top:var(--space-3)"></div>
<div class="detail" id="mClientMsg"></div>
</div>
</section>
</details>
<section class="card">
<h2>Trimise la suport <span class="count" id="mEscCount"></span></h2>
<div class="detail" id="mSupportDetail"></div>
<div id="mEsc" class="empty">se încarcă…</div>
</section>
<details class="card" data-panel="maria-docs">
<summary><span class="chev">▶</span><span class="ptitle">Documente indexate</span><span class="count" id="mDocCount"></span></summary>
<div class="pbody">
<div class="detail" id="mIndexDetail"></div>
<div class="detail" id="mSyncDetail"></div>
<div class="actions">
<button onclick="mReindex()">Reconstruiește indexul</button>
<button onclick="mSyncNow()">Sincronizează din Drive acum</button>
</div>
<div class="table-scroll">
<table class="stack-mobil">
<thead><tr><th>Nume</th><th class="num">Mărime</th><th></th></tr></thead>
<tbody id="mDocs"></tbody>
</table>
</div>
<h3>Adaugă document (.txt / .md)</h3>
<input type="text" id="mDocName" placeholder="ex: politica-retururi.md" style="width:100%;margin-bottom:var(--space-3)">
<textarea id="mDocContent" placeholder="conținutul documentului..." style="width:100%;min-height:120px;font-family:monospace"></textarea>
<div class="actions" style="margin-top:var(--space-3)">
<button class="primary" onclick="mAddDoc()">Salvează și reindexează</button>
</div>
<div class="detail" id="mDocMsg"></div>
</div>
</details>
<section class="card">
<h2>Fire de discuție active <span class="count" id="mFirCount"></span></h2>
<div class="table-scroll">
<table class="stack-mobil">
<thead><tr><th>JID</th><th>Subiect</th><th>Ultima activitate</th><th></th></tr></thead>
<tbody id="mFire"></tbody>
</table>
</div>
</section>
<section class="card">
<h2>Clienți cunoscuți <span class="count" id="mClientCount"></span></h2>
<div class="table-scroll">
<table class="stack-mobil">
<thead><tr><th>Nume</th><th>Numere</th><th></th><th></th></tr></thead>
<tbody id="mClients"></tbody>
</table>
</div>
<h2 style="margin-top:var(--space-5)" id="mClientFormTitle">Adaugă client</h2>
<input type="text" id="mClientId" placeholder="id (ex: acme)" style="width:100%;margin-bottom:var(--space-3)">
<input type="text" id="mClientNume" placeholder="nume firmă" style="width:100%;margin-bottom:var(--space-3)">
<textarea id="mClientProfil" placeholder="profil (context liber despre client)..." style="width:100%;min-height:80px;margin-bottom:var(--space-3)"></textarea>
<textarea id="mClientNumere" placeholder="numere de telefon, unul pe linie" style="width:100%;min-height:80px;font-family:monospace"></textarea>
<div class="actions" style="margin-top:var(--space-3)">
<button class="primary" onclick="mSaveClient()">Salvează</button>
<button class="small" onclick="mResetClientForm()">Renunță</button>
</div>
<div class="detail" id="mClientMsg"></div>
</section>
<section class="card">
<h2>Documente indexate <span class="count" id="mDocCount"></span></h2>
<div class="detail" id="mIndexDetail"></div>
<div class="detail" id="mSyncDetail"></div>
<div class="actions" style="margin:var(--space-5) 0">
<button onclick="mReindex()">Reconstruiește indexul</button>
<button onclick="mSyncNow()">Sincronizează din Drive acum</button>
</div>
<div class="table-scroll">
<table class="stack-mobil">
<thead><tr><th>Nume</th><th class="num">Mărime</th><th></th></tr></thead>
<tbody id="mDocs"></tbody>
</table>
</div>
<h2 style="margin-top:var(--space-5)">Adaugă document (.txt / .md)</h2>
<input type="text" id="mDocName" placeholder="ex: politica-retururi.md" style="width:100%;margin-bottom:var(--space-3)">
<textarea id="mDocContent" placeholder="conținutul documentului..." style="width:100%;min-height:120px;font-family:monospace"></textarea>
<div class="actions" style="margin-top:var(--space-3)">
<button class="primary" onclick="mAddDoc()">Salvează și reindexează</button>
</div>
<div class="detail" id="mDocMsg"></div>
</section>
<section class="card">
<h2>Jurnal Maria
<details class="card" data-panel="maria-log">
<summary><span class="chev">▶</span><span class="ptitle">Jurnal Maria</span>
<span class="count">
<button class="small" id="mTabBridge" onclick="mSetLog('bridge')">whatsapp.log</button>
<button class="small" id="mTabRag" onclick="mSetLog('rag')">rag.log</button>
<button class="small" onclick="mRefreshLogs()">↻</button>
</span>
</h2>
<pre class="log" id="mLog">se încarcă…</pre>
</section>
</summary>
<div class="pbody">
<pre class="log" id="mLog">se încarcă…</pre>
</div>
</details>
</div>
<!-- ══════════════════════ INFRASTRUCTURĂ ══════════════════════ -->
<div class="wrap tabpage" data-tab="infra">
<div class="banner" id="infraBanner" hidden></div>
<details class="card" id="infraLogPanel" data-panel="infra-log" hidden>
<summary><span class="chev">▶</span><span class="ptitle" id="infraLogTitle">Jurnal</span>
<span class="spacer"></span>
<button class="small" onclick="infraPollLog()">↻</button>
<button class="small danger" onclick="infraStopJob()">Oprește</button>
<button class="small" onclick="infraHideLog()">Închide</button>
</summary>
<div class="pbody">
<pre class="log" id="infraLogPre">se încarcă…</pre>
</div>
</details>
<details class="card" open data-panel="infra-stare">
<summary><span class="chev">▶</span><span class="ptitle">Stare cluster</span>
<span id="infraPills"></span>
<span class="spacer"></span><span class="psub">citire · la 30 s</span></summary>
<div class="pbody">
<div class="strip" id="infraStrip"></div>
<div class="tscroll table-scroll"><table>
<thead><tr><th>Nod</th><th>Stare</th><th class="num">CPU</th><th class="num">RAM</th><th class="num">Guest</th><th class="num">Uptime</th></tr></thead>
<tbody id="infraNodes"></tbody>
</table></div>
<div class="hint" id="infraErrors"></div>
</div>
</details>
<div class="cols">
<div class="col">
<details class="card" open data-panel="infra-oprire">
<summary><span class="chev">▶</span><span class="ptitle">Oprire cluster</span><span class="psub">cluster-shutdown.sh · 15–40 min</span>
<span class="spacer"></span>
<button onclick="infraRun('oprire', true)">Simulează</button>
<button class="danger" id="btnOprire" onclick="infraOprire()">Oprește…</button>
</summary>
<div class="pbody">
<ol class="steps">
<li>Verificări: ping, ssh, cvorum, fără task-uri în curs</li>
<li>Salvează lista guest-urilor pornite (.cluster-state.txt)</li>
<li><span><b>Trimite instrucțiunile de pornire</b> cât încă merge totul: mesaj în Discord + email</span></li>
<li>HA pe freeze; pauză la alerte, la testul DR și la fereastra de patch</li>
<li>Oprește guest-urile, Oracle 108 ultimul (shutdown immediate)</li>
<li class="dies">LXC 171 se oprește odată cu pvemini, la final: dashboard-ul și Discord nu mai răspund</li>
<li>pveelite → pve1 → pvemini; raportul final vine pe email</li>
</ol>
<div class="hint" id="infraOprireReason"></div>
</div>
</details>
<details class="card" open data-panel="infra-pornire">
<summary><span class="chev">▶</span><span class="ptitle">Pornire cluster</span><span class="psub">trezire de pe telefon, apoi de aici</span>
<span class="spacer"></span>
<button onclick="infraRun('pornire', true)">Simulează</button>
<button class="primary" id="btnPornire" onclick="infraPornire()">Pornește guest-urile</button>
</summary>
<div class="pbody">
<p class="hint">Cu dashboard-ul oprit, instrucțiunile de pornire se găsesc în Discord (mesaj fixat la oprire) și pe email. Textul lor e mai jos.</p>
<pre class="log" id="infraStartupText">—</pre>
<div class="actions">
<button onclick="infraCopyStartup()">Copiază comenzile de pornire</button>
<button onclick="infraRun('wol', false)">Trimite WoL către nodurile căzute</button>
</div>
</div>
</details>
<details class="card" data-panel="infra-ups">
<summary><span class="chev">▶</span><span class="ptitle">UPS</span><span id="infraUpsPill"></span></summary>
<div class="pbody rows">
<div class="row"><div><div class="t">Test rapid baterie</div><div class="d">ups-monthly-test.sh · ~2 min pe baterie</div></div><button id="btnUpsTest" onclick="infraConfirmRun('ups-test', false, 'Rulezi testul lunar UPS (descărcare controlată pe baterie)?')">Rulează</button></div>
<div class="row"><div><div class="t">Simulează oprirea de urgență</div><div class="d">ups-shutdown-cluster.sh --dry-run --force, ca nut</div></div><button id="btnUpsSim" onclick="infraRun('ups-simulare', false)">Simulează</button></div>
<div class="row"><div><div class="t">Istoric autonomie</div><div class="d">ultimele rânduri din trendul de baterie</div></div><button id="btnUpsHist" onclick="infraShowUpsHistory()">Vezi</button></div>
</div>
</details>
</div>
<div class="col">
<details class="card" open data-panel="infra-dr">
<summary><span class="chev">▶</span><span class="ptitle">DR · VM 109</span><span id="infraVm109Pill"></span></summary>
<div class="pbody rows">
<div class="row"><div><div class="t">Test DR acum</div><div class="d">restaurează backup-ul RMAN pe VM 109, verifică, oprește · ~25 min</div></div><button id="btnDrTest" onclick="infraConfirmRun('dr-test', false, 'Rulezi testul DR pe VM 109 acum?')">Rulează</button></div>
<div class="row"><div><div class="t">Fereastră de patch Windows</div><div class="d">vm109-patch-window.sh --now · 30–60 min</div></div><button id="btnDrPatch" onclick="infraConfirmRun('dr-patch', false, 'Pornești fereastra de patch VM 109?')">Rulează</button></div>
<div class="row"><div><div class="t">VM 109 manual</div><div class="d">pentru depanare; watchdog-ul intră pe pauză</div></div><span class="actions">
<button id="btnVm109On" onclick="infraConfirmRun('vm109-pornire', false, 'Pornești manual VM 109?')">Pornește</button>
<button class="danger" id="btnVm109Off" onclick="infraConfirmRun('vm109-oprire', false, 'Oprești VM 109?')">Oprește</button>
</span></div>
</div>
</details>
<details class="card danger" open data-panel="infra-failover">
<summary><span class="chev">▶</span><span class="ptitle">Failover și failback</span><span class="pill bad">distructiv</span><span class="psub">când un nod a căzut; deschide o operație pentru instrucțiuni</span></summary>
<div class="pbody">
<details class="op" open>
<summary><span class="chev">▶</span><span class="t">Backup-urile Oracle trec pe pvemini<span class="d">pveelite a căzut și nu revine curând</span></span><span id="infraFoPill1"></span></summary>
<dl class="sheet">
<dt>Situația</dt><dd>Serverul de producție (10.0.20.36) își copiază în fiecare noapte backup-urile RMAN pe <b>pveelite</b>. De acolo le folosește VM 109 la testul DR. Dacă pveelite e jos, copia din noaptea asta nu mai are unde ajunge.</dd>
<dt>Ce face</dt><dd>1. Verifică de două ori că pveelite chiar nu răspunde.<br>2. Pe pvemini, copia backup-urilor devine activă, cu scriere, partajată prin NFS.<br>3. Pe 10.0.20.36, transfer_backups.ps1 e redirecționat spre pvemini.<br>4. Oprește replicarea pveelite → pvemini.</dd>
<dt>Ce pierzi</dt><dd>Nimic din producție. Backup-urile ajunse pe pveelite după ultima replicare (maximum 15 min) rămân doar acolo.</dd>
<dt>Cum revii</dt><dd>Operația de mai jos, după ce pveelite a repornit.</dd>
<div class="foot"><span class="hint">failover-dr-to-pvemini.sh · pe pvemini · ~2 min</span><button class="danger" id="btnFailoverPvemini" onclick="infraConfirmRun('backup-pe-pvemini', false, 'pveelite a căzut: treci backup-urile Oracle pe pvemini?')">Rulează…</button></div>
</dl>
</details>
<details class="op">
<summary><span class="chev">▶</span><span class="t">Backup-urile Oracle revin pe pveelite<span class="d">după ce pveelite a repornit</span></span><span id="infraFoPill2"></span></summary>
<dl class="sheet">
<dt>Situația</dt><dd>Operația de mai sus a rulat. Între timp pvemini a primit backup-uri noi, iar pveelite e din nou pornit, dar are date vechi.</dd>
<dt>Ce face</dt><dd>1. Face un snapshot pe pvemini și îl trimite pe pveelite, suprascriind ce e acolo.<br>2. pvemini redevine replica read-only, fără NFS.<br>3. Pe 10.0.20.36, transferul e redirecționat înapoi spre pveelite.<br>4. Repornește replicarea.</dd>
<dt>Ce pierzi</dt><dd class="risk">Tot ce e pe pveelite și nu e pe pvemini. Nu rula dacă pveelite a mai primit backup-uri după căderea lui.</dd>
<dt>Cum revii</dt><dd>Nu există un pas invers; suprascrierea e definitivă.</dd>
<div class="foot"><span class="hint">failback-dr-to-pveelite.sh · pe pvemini · durează după volumul datelor</span><button class="danger" id="btnFailoverPveelite" onclick="infraConfirmRun('backup-pe-pveelite', false, 'pveelite e din nou online: aduci backup-urile Oracle înapoi pe pveelite? Suprascrie definitiv ce e pe pveelite.')">Rulează…</button></div>
</dl>
</details>
<details class="op obsolete">
<summary><span class="chev">▶</span><span class="t">VM 201 (roacentral) pe alt nod<span class="d">arhivat — vezi proxmox/cluster/failover/arhiva/</span></span><span class="pill warn">învechit</span></summary>
<dl class="sheet">
<dt>De ce nu</dt><dd>Scripturile au fost scrise când VM 201 nu era în HA. Din 2026-04-25 e în HA (ha-prefer-pvemini): dacă pvemini cade, se mută <b>singur pe pve1</b> și revine singur. Un failover manual ar intra în conflict cu HA.</dd>
<dt>În schimb</dt><dd>Nu apar ca butoane aici. Panoul „Stare cluster" arată doar unde rulează VM 201 și starea HA. Scripturile sunt arhivate în repo.</dd>
</dl>
</details>
</div>
</details>
<details class="card" data-panel="infra-discord">
<summary><span class="chev">▶</span><span class="ptitle">Discord · /infra</span><span class="psub">aceleași acțiuni, confirmare cu butoane</span></summary>
<div class="pbody">
<div class="cmds">
<code>/infra stare</code><span>starea clusterului, ca embed</span>
<code>/infra oprire · pornire</code><span>simulare implicit</span>
<code>/infra ups-test · ups-simulare</code><span>test rapid · dry-run urgență</span>
<code>/infra dr-test · dr-patch · vm109</code><span>VM 109</span>
<code>/infra backup-pe-pvemini · backup-pe-pveelite</code><span>arată fișa de instrucțiuni, apoi butonul</span>
<code>/infra sarcini</code><span>ce rulează + ultimele linii din jurnal</span>
</div>
</div>
</details>
</div>
</div>
</div>
@@ -240,6 +420,42 @@ function mb(bytes) {
return bytes === null || bytes === undefined ? '—' : (bytes / 1048576).toFixed(0) + ' MB';
}
// ── tab-uri + panouri colapsabile ───────────────────────────────
function activeTab() {
var h = (location.hash || '').replace('#', '');
return ['punte', 'maria', 'infra'].indexOf(h) >= 0 ? h : 'punte';
}
function showTab(t) {
document.querySelectorAll('.tab').forEach(function (b) { b.classList.toggle('on', b.dataset.tab === t); });
document.querySelectorAll('.tabpage').forEach(function (p) { p.classList.toggle('on', p.dataset.tab === t); });
if (t === 'infra') { startInfraPolling(); } else { stopInfraPolling(); }
}
document.querySelectorAll('.tab').forEach(function (b) {
b.addEventListener('click', function () { location.hash = b.dataset.tab; });
});
window.addEventListener('hashchange', function () { showTab(activeTab()); });
document.querySelectorAll('details.card').forEach(function (d) {
var key = 'panel:' + (d.dataset.panel || '');
try {
var saved = localStorage.getItem(key);
if (saved !== null) d.open = saved === '1';
} catch (e) { /* localStorage indisponibil (mod privat) */ }
d.addEventListener('toggle', function () {
try { localStorage.setItem(key, d.open ? '1' : '0'); } catch (e) { /* ignorat */ }
});
});
// Butoanele din summary nu trebuie sa deschida/inchida panoul.
document.querySelectorAll('summary button').forEach(function (b) {
b.addEventListener('click', function (e) { e.preventDefault(); });
});
function toggleAllPanels() {
var panels = [].slice.call(document.querySelectorAll('details.card:not([hidden])'));
var anyOpen = panels.some(function (p) { return p.open; });
panels.forEach(function (p) { p.open = !anyOpen; });
document.getElementById('btnToggleAll').textContent = anyOpen ? 'Extinde tot' : 'Restrânge tot';
}
// ── stare ─────────────────────────────────────────────────────
var busy = false;
async function refresh() {
@@ -636,7 +852,235 @@ async function mRefreshLogs() {
el.scrollTop = el.scrollHeight;
}
// ── Infrastructură ────────────────────────────────────────────
var infraVisible = false;
var infraTimer = null;
var infraLast = null;
var infraBusy = {};
function startInfraPolling() {
if (infraVisible) return;
infraVisible = true;
refreshInfra();
infraTimer = setInterval(refreshInfra, 30000);
}
function stopInfraPolling() {
infraVisible = false;
if (infraTimer) { clearInterval(infraTimer); infraTimer = null; }
}
async function refreshInfra(fresh) {
var res = await api('api/infra/status' + (fresh ? '?fresh=1' : ''));
if (!res.ok) return;
infraLast = res.data;
renderInfra(res.data);
}
function infraActionOk(id) {
var a = infraLast && infraLast.actions && infraLast.actions[id];
return !!(a && a.ok);
}
function infraActionReason(id) {
var a = infraLast && infraLast.actions && infraLast.actions[id];
return a && !a.ok ? a.reason : '';
}
function infraSetBtn(id, btnId) {
var el = document.getElementById(btnId);
if (!el) return;
var ok = infraActionOk(id);
el.disabled = !ok || !!infraBusy[id];
el.title = ok ? '' : infraActionReason(id);
}
function renderInfra(s) {
// banner joburi active
var jobs = (s.jobs || []).filter(function (j) { return j.active; });
var banner = document.getElementById('infraBanner');
if (jobs.length) {
banner.hidden = false;
banner.innerHTML = jobs.map(function (j) {
return '<span class="pill busy">● rulează</span><b>' + esc(j.id) + '</b>' +
'<span class="mono muted">@ ' + esc(j.host) + '</span>' +
'<span class="spacer"></span>' +
'<button onclick="infraShowLog(\'' + esc(j.id) + '\')">Jurnal</button>';
}).join('');
} else {
banner.hidden = true;
banner.innerHTML = '';
}
// pastile pe titlul "Stare cluster"
var q = s.quorum || {};
var g = s.guests || {};
var repl = s.replication || {};
var pills = [];
pills.push('<span class="pill ' + (q.quorate ? 'ok' : 'bad') + '">cvorum ' + (q.votes || 0) + '/' + (q.expected || 0) + '</span>');
pills.push('<span class="pill ' + (g.total && g.running === g.total ? 'ok' : 'warn') + '">' + (g.running || 0) + '/' + (g.total || 0) + ' guest-uri</span>');
if (repl.total) pills.push('<span class="pill ' + (repl.ok === repl.total ? 'ok' : 'bad') + '">replicare ' + repl.ok + '/' + repl.total + '</span>');
if (s.ssl) pills.push('<span class="pill ' + (s.ssl.min_days <= 14 ? 'warn' : 'ok') + '">SSL ' + s.ssl.min_days + ' zile</span>');
document.getElementById('infraPills').innerHTML = pills.join('');
// banda de metrici
var strip = [];
strip.push(['Cvorum', (q.votes || 0) + ' / ' + (q.expected || 0), 'voturi', false]);
strip.push(['Guest-uri', (g.running || 0) + ' / ' + (g.total || 0), '', false]);
if (s.vm109) strip.push(['VM 109', s.vm109.status || '—', s.vm109.node || '', false]);
if (repl.total) strip.push(['Replicare', repl.ok + ' / ' + repl.total, repl.failed && repl.failed.length ? repl.failed.join(', ') : '', repl.ok !== repl.total]);
if (s.ups) strip.push(['UPS', (s.ups.charge || 0) + '%', s.ups.status || '', s.ups.status !== 'OL']);
if (s.backup) strip.push(['Backup Oracle', s.backup.full_age_h != null ? s.backup.full_age_h.toFixed(1) + ' h' : '—', s.backup.cum_age_h != null ? 'CUM ' + s.backup.cum_age_h.toFixed(1) + ' h' : '', false]);
if (s.ssl) strip.push(['SSL minim', s.ssl.min_days + ' zile', s.ssl.domain, s.ssl.min_days <= 14]);
document.getElementById('infraStrip').innerHTML = strip.map(function (m) {
return '<div' + (m[3] ? ' class="warn"' : '') + '><div class="l">' + esc(m[0]) + '</div><div class="v">' + esc(m[1]) + '</div><div class="s">' + esc(m[2]) + '</div></div>';
}).join('');
// tabel noduri
document.getElementById('infraNodes').innerHTML = (s.nodes || []).map(function (n) {
return '<tr><td><b>' + esc(n.name) + '</b> <span class="mono muted">' + esc(n.ip) + '</span></td>' +
'<td><span class="pill ' + (n.online ? 'ok' : 'bad') + '">' + (n.online ? 'online' : 'offline') + '</span></td>' +
'<td class="num">' + (n.online ? (n.cpu * 100).toFixed(0) + '%' : '—') + '</td>' +
'<td class="num">' + (n.online && n.maxmem ? (n.mem / 1073741824).toFixed(0) + '/' + (n.maxmem / 1073741824).toFixed(0) + ' GB' : '—') + '</td>' +
'<td class="num">' + (n.online ? n.guests : '—') + '</td>' +
'<td class="num">' + (n.online ? dur(n.uptime) : '—') + '</td></tr>';
}).join('');
var errs = Object.keys(s.errors || {});
document.getElementById('infraErrors').textContent = errs.length
? 'Erori la citire: ' + errs.map(function (h) { return h + ' (' + s.errors[h] + ')'; }).join(', ')
: '';
// butoane oprire/pornire
infraSetBtn('oprire', 'btnOprire');
infraSetBtn('pornire', 'btnPornire');
document.getElementById('infraOprireReason').textContent = infraActionOk('oprire') ? '' : ('Blocat: ' + infraActionReason('oprire'));
document.getElementById('infraStartupText').textContent = s.startup_text || '—';
// UPS
var ups = s.ups;
document.getElementById('infraUpsPill').innerHTML = ups
? '<span class="pill ' + (ups.status === 'OL' ? 'ok' : 'bad') + '">' + esc(ups.status || '?') + ' ' + (ups.charge || 0) + '%</span>'
: '<span class="pill bad">necunoscut</span>';
infraSetBtn('ups-test', 'btnUpsTest');
infraSetBtn('ups-simulare', 'btnUpsSim');
infraSetBtn('ups-istoric', 'btnUpsHist');
// VM 109 / DR
var vm109 = s.vm109;
document.getElementById('infraVm109Pill').innerHTML = vm109
? '<span class="pill ' + (vm109.status === 'running' ? 'busy' : '') + '">' + esc(vm109.status || '?') + '</span>'
: '<span class="pill bad">necunoscut</span>';
infraSetBtn('dr-test', 'btnDrTest');
infraSetBtn('dr-patch', 'btnDrPatch');
infraSetBtn('vm109-pornire', 'btnVm109On');
infraSetBtn('vm109-oprire', 'btnVm109Off');
// failover/failback
infraSetBtn('backup-pe-pvemini', 'btnFailoverPvemini');
infraSetBtn('backup-pe-pveelite', 'btnFailoverPveelite');
document.getElementById('infraFoPill1').innerHTML = infraActionOk('backup-pe-pvemini')
? '<span class="pill ok">disponibil</span>' : '<span class="pill bad">' + esc(infraActionReason('backup-pe-pvemini')) + '</span>';
document.getElementById('infraFoPill2').innerHTML = infraActionOk('backup-pe-pveelite')
? '<span class="pill ok">disponibil</span>' : '<span class="pill bad">' + esc(infraActionReason('backup-pe-pveelite')) + '</span>';
// banner de job -> daca un job tocmai a pornit, deschide jurnalul automat
if (infraAutoOpenLog) { infraShowLog(infraAutoOpenLog); infraAutoOpenLog = null; }
}
async function infraRun(id, dryRun) {
if (infraBusy[id]) return;
infraBusy[id] = true;
try {
var res = await post('api/infra/run', { id: id, dry_run: !!dryRun });
if (res.ok) {
toast((dryRun ? 'Simulare pornită: ' : 'Pornit: ') + id + (res.data.log ? ' · jurnal disponibil' : ''), 'ok');
if (res.data.log) { infraAutoOpenLog = id; }
else if (res.data.output) { toast(String(res.data.output).slice(0, 300), ''); }
} else {
toast('Eșuat (' + res.status + '): ' + (res.data.error || ''), 'bad');
}
} finally {
infraBusy[id] = false;
refreshInfra(true);
}
}
var infraAutoOpenLog = null;
async function infraConfirmRun(id, dryRun, msg) {
if (!dryRun && !confirm(msg || ('Sigur rulezi „' + id + '”?'))) return;
return infraRun(id, dryRun);
}
async function infraOprire() {
if (!infraActionOk('oprire')) { toast('Blocat: ' + infraActionReason('oprire'), 'bad'); return; }
var typed = prompt('Oprești tot clusterul (LXC 171 se oprește la final; dashboard-ul devine inaccesibil). Scrie OPRESTE ca să confirmi:');
if (typed !== 'OPRESTE') { toast('Anulat', ''); return; }
return infraRun('oprire', false);
}
async function infraPornire() {
if (!infraActionOk('pornire')) { toast('Blocat: ' + infraActionReason('pornire'), 'bad'); return; }
if (!confirm('Pornești guest-urile clusterului (Oracle primul, crontab-urile la loc)?')) return;
return infraRun('pornire', false);
}
function infraCopyStartup() {
var text = (infraLast && infraLast.startup_text) || '';
try {
navigator.clipboard.writeText(text);
toast('Copiat în clipboard', 'ok');
} catch (e) {
toast('Nu am putut copia — selectează textul manual', 'bad');
}
}
async function infraShowUpsHistory() {
var res = await post('api/infra/run', { id: 'ups-istoric', dry_run: false });
if (res.ok && res.data.output) {
infraShowLog(null);
document.getElementById('infraLogTitle').textContent = 'Istoric autonomie UPS';
document.getElementById('infraLogPre').textContent = res.data.output;
document.getElementById('infraLogPanel').open = true;
} else {
toast('Eșuat: ' + (res.data.error || res.status), 'bad');
}
}
var infraLogTimer = null;
var infraLogId = null;
function infraShowLog(id) {
infraLogId = id;
var panel = document.getElementById('infraLogPanel');
panel.hidden = false;
panel.open = true;
document.getElementById('infraLogTitle').textContent = id ? ('Jurnal: ' + id) : 'Jurnal';
if (infraLogTimer) clearInterval(infraLogTimer);
if (id) {
infraPollLog();
infraLogTimer = setInterval(infraPollLog, 3000);
}
}
function infraHideLog() {
document.getElementById('infraLogPanel').hidden = true;
if (infraLogTimer) { clearInterval(infraLogTimer); infraLogTimer = null; }
infraLogId = null;
}
async function infraPollLog() {
if (!infraLogId) return;
var res = await api('api/infra/log?id=' + encodeURIComponent(infraLogId) + '&n=100');
if (!res.ok) return;
var el = document.getElementById('infraLogPre');
el.textContent = (res.data.lines || []).join('\n') || '(gol)';
el.scrollTop = el.scrollHeight;
}
async function infraStopJob() {
if (!infraLogId) { infraHideLog(); return; }
if (!confirm('Oprești jobul „' + infraLogId + '”? Dacă e o oprire/pornire de cluster în curs, clusterul poate rămâne pe jumătate configurat.')) return;
var res = await post('api/infra/stop', { id: infraLogId });
toast(res.ok ? 'Job oprit' : 'Eșuat: ' + (res.data.error || res.status), res.ok ? 'ok' : 'bad');
refreshInfra(true);
}
// ── pornire ───────────────────────────────────────────────────
showTab(activeTab());
refresh(); refreshDoctor(); refreshLogs();
mRefresh(); mRefreshLogs(); mLoadClients(); mLoadFire();
setInterval(refresh, 5000);