@@ -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 = " aproba ri" >
< 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 ')" > Opr ește< / button >
< button class = "primary " onclick = "mSvc('bridge','restart ')" > Reporn eș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 ')" > Porn ește< / button >
< button class = "danger " onclick = "mSvc('bridge','stop ')" > Opr eș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 ')" > Opr ește< / button >
< button class = "primary " onclick = "mSvc('rag','restart ')" > Reporn eș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 ')" > Porn ește< / button >
< button class = "danger " onclick = "mSvc('rag','stop ')" > Opr eș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= "empt y" > se încarcă…< / div >
< div id = "mWaQr" > < / div >
< div id = "mWaPair" hidden >
< div class = "det ail " style = "margin-top:var(--space-5)" >
Nu poți scana codul QR? Cere un < strong > cod de asociere< / strong > și
intr odu- 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 = "pbod y" >
< div id = "mWaStatus" class = "empty" > se încarcă… < / div >
< div id = "mWaQr" > < / div >
< div id = "mWaP air " hidden >
< div class = "detail" >
Nu poți scana c odul 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 = "mEs cCount" > < / 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 = "mDo cCount" > < / 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 ) ;