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:
@@ -44,6 +44,7 @@ for _p in (str(_BRIDGE), str(_DASH)):
|
||||
sys.path.insert(0, _p)
|
||||
|
||||
import config # noqa: E402
|
||||
import infra_actions # noqa: E402
|
||||
from limits import parse_cap # noqa: E402
|
||||
|
||||
# ── constante ───────────────────────────────────────────────────────────
|
||||
@@ -847,6 +848,18 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return self.send_json({"clients": maria_clients()})
|
||||
if path == "/api/maria/fire":
|
||||
return self.send_json({"fire": maria_fire()})
|
||||
if path == "/api/infra/status":
|
||||
fresh = qs.get("fresh", ["0"])[0] not in ("0", "", "false")
|
||||
return self.send_json(infra_actions.status(fresh=fresh))
|
||||
if path == "/api/infra/log":
|
||||
action_id = qs.get("id", [""])[0]
|
||||
if action_id not in infra_actions.ACTIONS:
|
||||
return self.send_json({"error": "actiune necunoscuta"}, 400)
|
||||
try:
|
||||
n = min(max(int(qs.get("n", ["60"])[0]), 1), 2000)
|
||||
except ValueError:
|
||||
n = 60
|
||||
return self.send_json(infra_actions.log_tail(action_id, n))
|
||||
if path == "/api/maria/logs":
|
||||
try:
|
||||
n = min(max(int(qs.get("lines", ["200"])[0]), 1), 2000)
|
||||
@@ -874,6 +887,10 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return self.deny()
|
||||
if path == "/api/service":
|
||||
return self.handle_service()
|
||||
if path == "/api/infra/run":
|
||||
return self.handle_infra_run()
|
||||
if path == "/api/infra/stop":
|
||||
return self.handle_infra_stop()
|
||||
if path == "/api/cleanup":
|
||||
data = self.read_json()
|
||||
return self.send_json(orphans_report(dry_run=bool(data.get("dry_run", True))))
|
||||
@@ -959,6 +976,26 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return self.send_json({"ok": bool(ok), "request_id": rid, "decision": decision},
|
||||
200 if ok else 404)
|
||||
|
||||
def handle_infra_run(self):
|
||||
data = self.read_json()
|
||||
action_id = str(data.get("id") or "")
|
||||
dry_run = bool(data.get("dry_run"))
|
||||
who = tailnet_user(self.headers) or "local"
|
||||
print(f"[actiune] infra {action_id} dry={dry_run}, cerut de {who}",
|
||||
file=sys.stderr, flush=True)
|
||||
res = infra_actions.run(action_id, dry_run)
|
||||
return self.send_json(res, res["code"])
|
||||
|
||||
def handle_infra_stop(self):
|
||||
data = self.read_json()
|
||||
action_id = str(data.get("id") or "")
|
||||
if action_id not in infra_actions.ACTIONS:
|
||||
return self.send_json({"ok": False, "code": 400, "error": "actiune necunoscuta"}, 400)
|
||||
who = tailnet_user(self.headers) or "local"
|
||||
print(f"[actiune] infra stop {action_id}, cerut de {who}", file=sys.stderr, flush=True)
|
||||
res = infra_actions.stop(action_id)
|
||||
return self.send_json(res, res["code"])
|
||||
|
||||
def handle_maria_service(self):
|
||||
"""start / stop / restart pe una din UNITATILE FIXE ale Mariei — acelasi
|
||||
principiu ca handle_service: numele vine dintr-o cheie (bridge/rag), nu
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
@@ -358,3 +359,114 @@ input[type="password"]:focus { outline: none; border-color: var(--border-focus);
|
||||
.actions { grid-template-columns: 1fr; }
|
||||
.metrics { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
|
||||
/* ── doua coloane (tab Infrastructura) ────────────────────────────────── */
|
||||
.cols { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fit, minmax(min(400px, 100%), 1fr)); align-items: start; }
|
||||
.col { display: grid; gap: var(--space-4); min-width: 0; }
|
||||
.tscroll { overflow-x: auto; }
|
||||
.psub { color: var(--text-muted); font-size: var(--text-xs); }
|
||||
|
||||
/* ── tab-uri (Punte / Maria / Infrastructura) ─────────────────────────── */
|
||||
.tabs { display: flex; gap: 2px; }
|
||||
.tab {
|
||||
padding: 2px var(--space-3);
|
||||
border-radius: 999px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
font-size: var(--text-xs);
|
||||
border: 1px solid transparent;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tab.on { color: var(--text-primary); border-color: var(--border-focus); background: var(--accent-subtle); }
|
||||
.tabpage { display: none; }
|
||||
.tabpage.on { display: grid; }
|
||||
|
||||
/* ── panouri colapsabile (details.card inlocuieste vechiul section.card) ── */
|
||||
details.card { padding: 0; }
|
||||
details.card.danger { border-color: rgba(239, 68, 68, 0.5); }
|
||||
details.card > summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2) var(--space-3);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
user-select: none;
|
||||
border-radius: var(--radius-lg);
|
||||
margin: 0;
|
||||
}
|
||||
details.card > summary::-webkit-details-marker { display: none; }
|
||||
details.card > summary:hover { background: var(--bg-surface-hover); }
|
||||
details.card > summary:focus-visible { outline: 2px solid var(--border-focus); outline-offset: -2px; }
|
||||
details.card .chev { width: 14px; color: var(--text-muted); transition: transform .15s; flex: none; font-size: 10px; }
|
||||
details.card[open] > summary .chev { transform: rotate(90deg); }
|
||||
details.card .ptitle { font-weight: 600; color: var(--text-primary); font-size: var(--text-base); }
|
||||
.pbody { padding: 0 var(--space-5) var(--space-5); display: grid; gap: var(--space-4); border-top: 1px solid var(--border); }
|
||||
.pbody > :first-child { margin-top: var(--space-4); }
|
||||
@media (prefers-reduced-motion: reduce) { details.card .chev { transition: none; } }
|
||||
|
||||
/* ── banda de metrici compacta (stare cluster) ────────────────────────── */
|
||||
.strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
||||
.strip > div { padding: var(--space-2) var(--space-3); border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); margin: 0 -1px -1px 0; }
|
||||
.strip .l { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); }
|
||||
.strip .v { font-size: var(--text-base); font-weight: 600; color: var(--text-primary); font-variant-numeric: tabular-nums; }
|
||||
.strip .s { font-size: var(--text-xs); color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.strip .warn .v { color: var(--warning); }
|
||||
|
||||
/* ── randuri de actiune (UPS, DR VM109) ───────────────────────────────── */
|
||||
.rows { display: grid; }
|
||||
.row { display: grid; grid-template-columns: 1fr auto; gap: 2px var(--space-3); align-items: center; padding: var(--space-2) 0; border-top: 1px solid var(--border); }
|
||||
.row:first-child { border-top: 0; }
|
||||
.row .t { font-weight: 600; color: var(--text-primary); }
|
||||
.row .d { color: var(--text-muted); font-size: var(--text-xs); }
|
||||
|
||||
/* ── pasi numerotati (oprire/pornire) ─────────────────────────────────── */
|
||||
ol.steps { margin: 0; padding: 0; list-style: none; counter-reset: s; display: grid; }
|
||||
ol.steps li { counter-increment: s; display: grid; grid-template-columns: 18px 1fr; gap: var(--space-2); padding: 3px 0; font-size: var(--text-xs); }
|
||||
ol.steps li::before { content: counter(s); color: var(--text-muted); font-family: var(--font-mono); text-align: right; }
|
||||
ol.steps li.dies { color: var(--error); }
|
||||
ol.steps li.dies::before { content: "!"; color: var(--error); font-weight: 700; }
|
||||
ol.steps li.here::before { content: "▸"; color: var(--accent); }
|
||||
.cmt { color: var(--text-muted); }
|
||||
|
||||
/* ── banner de job activ ──────────────────────────────────────────────── */
|
||||
.banner { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); align-items: center; background: var(--accent-subtle); border: 1px solid var(--border-focus); border-radius: var(--radius-lg); padding: var(--space-2) var(--space-3); font-size: var(--text-xs); }
|
||||
.hint { font-size: var(--text-xs); color: var(--text-muted); }
|
||||
.hint.warn { color: var(--warning); }
|
||||
|
||||
/* ── fisa de operatie (failover/failback) ─────────────────────────────── */
|
||||
details.op { border-top: 1px solid var(--border); }
|
||||
details.op:first-of-type { border-top: 0; }
|
||||
details.op > summary {
|
||||
list-style: none; cursor: pointer;
|
||||
display: grid; grid-template-columns: 14px 1fr auto; gap: var(--space-2); align-items: center;
|
||||
padding: var(--space-3) 0; margin: 0;
|
||||
}
|
||||
details.op > summary::-webkit-details-marker { display: none; }
|
||||
details.op > summary .t { font-weight: 600; color: var(--text-primary); }
|
||||
details.op > summary .d { display: block; color: var(--text-muted); font-size: var(--text-xs); font-weight: 400; }
|
||||
details.op > summary:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; }
|
||||
.sheet { margin: 0 0 var(--space-3) 22px; display: grid; grid-template-columns: 110px 1fr; gap: var(--space-2) var(--space-3); font-size: var(--text-xs); background: var(--bg-surface-hover); border-radius: var(--radius); padding: var(--space-3); }
|
||||
.sheet dt { color: var(--text-muted); font-size: 10px; text-transform: uppercase; letter-spacing: .05em; padding-top: 2px; margin: 0; }
|
||||
.sheet dd { margin: 0; }
|
||||
.sheet dd.risk { color: var(--error); }
|
||||
.sheet .foot { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; justify-content: space-between; border-top: 1px solid var(--border); padding-top: var(--space-2); margin-top: 2px; }
|
||||
.obsolete { background: rgba(239, 68, 68, 0.06); }
|
||||
:root[data-theme="light"] .obsolete { background: rgba(220, 38, 38, 0.05); }
|
||||
|
||||
/* ── ajutor Discord /infra ─────────────────────────────────────────────── */
|
||||
.cmds { display: grid; grid-template-columns: auto 1fr; gap: 2px var(--space-3); font-size: var(--text-xs); }
|
||||
.cmds code { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-primary); white-space: nowrap; }
|
||||
.cmds span { color: var(--text-muted); }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.row { grid-template-columns: 1fr; }
|
||||
.sheet { grid-template-columns: 1fr; margin-left: 0; }
|
||||
.sheet dt { padding-top: var(--space-1); }
|
||||
.cmds { grid-template-columns: 1fr; }
|
||||
.cmds span { margin-bottom: var(--space-1); }
|
||||
details.card > summary { padding: var(--space-3) var(--space-4); }
|
||||
.pbody { padding: 0 var(--space-4) var(--space-4); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user