Consolidate Maria control into the shared Discord-bridge dashboard
Drop the standalone maria-dashboard.service — one common panel now controls both bridges, at the operator's request. discord-bridge's dashboard/api.py gains a Maria section (fixed-unit start/stop/restart for maria-whatsapp/maria-rag, WhatsApp status+QR proxy, document CRUD, reindex, Drive sync trigger, log tail) reached via subprocess/HTTP — no cross-module imports, to avoid colliding with discord-bridge's own `config` module name. index.html gets a matching "Maria — WhatsApp + RAG" section. maria-whatsapp-bridge loses its dashboard/ folder and DASHBOARD_* env keys; install.sh/README point at the shared panel. Co-Authored-By: Claude Agent <noreply@anthropic.com>
This commit is contained in:
@@ -202,6 +202,12 @@ https://claude-agent.tailf7372d.ts.net/claude # din tailnet (ca /echo la molt
|
||||
Procesul e legat de `127.0.0.1:18790`; in tailnet il publica `tailscale serve`.
|
||||
Fara Tailscale: `ssh -L 18790:127.0.0.1:18790 -N claude@10.0.20.171`.
|
||||
|
||||
Acelasi panou include si sectiunea **"Maria — WhatsApp + RAG"**, care controleaza
|
||||
proiectul sibling `../maria-whatsapp-bridge/` (punte WhatsApp Baileys + consumer
|
||||
RAG): start/stop/restart, cod QR de asociere, depozit de documente, reindexare
|
||||
si sincronizare Google Drive. Decizie deliberata: un singur dashboard comun
|
||||
pentru ambele punti de pe acest container, nu un panou separat per serviciu.
|
||||
|
||||
| Fisier | Ce e |
|
||||
|--------|------|
|
||||
| `~/.claude-discord/env` | token + allowlist + limite (0600) |
|
||||
|
||||
@@ -21,12 +21,15 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
@@ -47,6 +50,120 @@ from limits import parse_cap # noqa: E402
|
||||
SERVICE = "claude-discord.service"
|
||||
SELF_SERVICE = "claude-discord-dashboard.service"
|
||||
|
||||
# ── Maria (WhatsApp+RAG, proiect sibling in acelasi repo) ────────────────
|
||||
# Panou unic la cererea operatorului: acest dashboard controleaza si serviciile
|
||||
# Mariei, nu doar puntea Discord. NU importam rag/config.py de acolo (ar coliza
|
||||
# pe numele de modul "config" cu discord-bridge/config.py, deja importat mai
|
||||
# sus) — citim direct caile si folosim subprocess/HTTP, exact ca la SERVICE.
|
||||
MARIA_UNITS = {"bridge": "maria-whatsapp.service", "rag": "maria-rag.service"}
|
||||
MARIA_STATE_DIR = Path(os.environ.get("MARIA_BRIDGE_DIR") or (Path.home() / ".maria-bridge"))
|
||||
MARIA_DOCS_DIR = MARIA_STATE_DIR / "documents"
|
||||
MARIA_INDEX_FILE = MARIA_STATE_DIR / "rag_index.json"
|
||||
MARIA_ENV_FILE = MARIA_STATE_DIR / "env"
|
||||
MARIA_LOG_DIR = MARIA_STATE_DIR / "logs"
|
||||
MARIA_VENV_PY = MARIA_STATE_DIR / "venv" / "bin" / "python"
|
||||
MARIA_RAG_DIR = _BRIDGE.parent / "maria-whatsapp-bridge" / "rag"
|
||||
_MARIA_SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]{1,200}$")
|
||||
|
||||
|
||||
def maria_env() -> dict:
|
||||
try:
|
||||
return config.parse_env(MARIA_ENV_FILE.read_text(encoding="utf-8"))
|
||||
except OSError:
|
||||
return {}
|
||||
|
||||
|
||||
def maria_get(key: str, default: str = "") -> str:
|
||||
return maria_env().get(key, default)
|
||||
|
||||
|
||||
def maria_bridge_url() -> str:
|
||||
return f"http://{maria_get('BRIDGE_HOST', '127.0.0.1')}:{maria_get('BRIDGE_PORT', '8099')}"
|
||||
|
||||
|
||||
def maria_log(which: str) -> Path:
|
||||
name = {"bridge": "whatsapp.log", "rag": "rag.log"}.get(which, "rag.log")
|
||||
return MARIA_LOG_DIR / name
|
||||
|
||||
|
||||
def maria_whatsapp_status() -> dict:
|
||||
try:
|
||||
req = urllib.request.Request(f"{maria_bridge_url()}/status")
|
||||
with urllib.request.urlopen(req, timeout=3) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except (urllib.error.URLError, TimeoutError, OSError, ValueError):
|
||||
return {"connected": False, "phone": None, "qr": None, "reachable": False}
|
||||
|
||||
|
||||
def maria_index_info() -> dict:
|
||||
try:
|
||||
entries = json.loads(MARIA_INDEX_FILE.read_text(encoding="utf-8"))
|
||||
sources = {e.get("source") for e in entries if isinstance(e, dict) and e.get("source")}
|
||||
return {"chunks": len(entries), "documents_indexed": len(sources),
|
||||
"mtime": MARIA_INDEX_FILE.stat().st_mtime}
|
||||
except (OSError, ValueError):
|
||||
return {"chunks": 0, "documents_indexed": 0, "mtime": None}
|
||||
|
||||
|
||||
def maria_sync_state() -> dict:
|
||||
try:
|
||||
return json.loads((MARIA_STATE_DIR / ".sync_state.json").read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def maria_documents() -> list[dict]:
|
||||
MARIA_DOCS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
out = []
|
||||
for f in sorted(MARIA_DOCS_DIR.glob("*")):
|
||||
if f.is_file() and f.suffix in (".txt", ".md"):
|
||||
st = f.stat()
|
||||
out.append({"name": f.name, "size": st.st_size, "mtime": st.st_mtime})
|
||||
return out
|
||||
|
||||
|
||||
def _maria_validate_name(name: str) -> str:
|
||||
if not name or not _MARIA_SAFE_NAME.match(name) or ".." in name or "/" in name:
|
||||
raise ValueError(f"nume de document invalid: {name!r}")
|
||||
if not name.endswith((".txt", ".md")):
|
||||
raise ValueError("doar fisiere .txt sau .md")
|
||||
return name
|
||||
|
||||
|
||||
def maria_write_document(name: str, content: str) -> None:
|
||||
name = _maria_validate_name(name)
|
||||
MARIA_DOCS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
(MARIA_DOCS_DIR / name).write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def maria_delete_document(name: str) -> bool:
|
||||
name = _maria_validate_name(name)
|
||||
path = MARIA_DOCS_DIR / name
|
||||
if not path.exists():
|
||||
return False
|
||||
path.unlink()
|
||||
return True
|
||||
|
||||
|
||||
def _maria_run(script: str, timeout: float = 600.0, extra_args: list[str] | None = None) -> dict:
|
||||
interpreter = str(MARIA_VENV_PY) if MARIA_VENV_PY.exists() else sys.executable
|
||||
args = [interpreter, str(MARIA_RAG_DIR / script), *(extra_args or [])]
|
||||
try:
|
||||
r = subprocess.run(args, capture_output=True, text=True, timeout=timeout, cwd=str(MARIA_RAG_DIR))
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"ok": False, "error": f"{script} a depasit timpul ({timeout:.0f}s)"}
|
||||
return {"ok": r.returncode == 0, "stdout": r.stdout[-4000:], "stderr": r.stderr[-4000:]}
|
||||
|
||||
|
||||
def maria_reindex() -> dict:
|
||||
return _maria_run("indexer.py")
|
||||
|
||||
|
||||
def maria_sync(force: bool = False) -> dict:
|
||||
out = _maria_run("sync.py", extra_args=(["--force"] if force else []))
|
||||
out["state"] = maria_sync_state()
|
||||
return out
|
||||
|
||||
|
||||
# Caile se recalculeaza la fiecare apel, nu se ingheata la import: `config.reload()`
|
||||
# muta STATE_DIR (testele o folosesc ca sa scoata totul din ~/.claude-discord).
|
||||
@@ -526,6 +643,28 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return self.send_json({"checks": doctor()})
|
||||
if path == "/api/cleanup":
|
||||
return self.send_json(orphans_report(dry_run=True))
|
||||
if path == "/api/maria/status":
|
||||
return self.send_json({
|
||||
"bridge": unit_info(MARIA_UNITS["bridge"]),
|
||||
"rag": unit_info(MARIA_UNITS["rag"]),
|
||||
"whatsapp": maria_whatsapp_status(),
|
||||
"index": maria_index_info(),
|
||||
"documents": len(maria_documents()),
|
||||
"sync": {**maria_sync_state(), "drive_remote": maria_get("DRIVE_REMOTE") or None},
|
||||
})
|
||||
if path == "/api/maria/documents":
|
||||
return self.send_json({"documents": maria_documents()})
|
||||
if path == "/api/maria/logs":
|
||||
try:
|
||||
n = min(max(int(qs.get("lines", ["200"])[0]), 1), 2000)
|
||||
except ValueError:
|
||||
n = 200
|
||||
target = maria_log(qs.get("service", ["bridge"])[0])
|
||||
if not target.exists():
|
||||
return self.send_json({"lines": [f"({target} nu exista)"]})
|
||||
r = subprocess.run(["tail", "-n", str(n), str(target)],
|
||||
capture_output=True, text=True, timeout=15)
|
||||
return self.send_json({"file": target.name, "lines": r.stdout.splitlines()})
|
||||
return self.send_json({"error": "ruta necunoscuta"}, 404)
|
||||
|
||||
# --- POST ----------------------------------------------------------
|
||||
@@ -547,6 +686,17 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return self.send_json(orphans_report(dry_run=bool(data.get("dry_run", True))))
|
||||
if path == "/api/approvals/decide":
|
||||
return self.handle_decide()
|
||||
if path == "/api/maria/service":
|
||||
return self.handle_maria_service()
|
||||
if path == "/api/maria/documents":
|
||||
return self.handle_maria_document_write()
|
||||
if path == "/api/maria/documents/delete":
|
||||
return self.handle_maria_document_delete()
|
||||
if path == "/api/maria/reindex":
|
||||
return self.send_json(maria_reindex())
|
||||
if path == "/api/maria/sync":
|
||||
data = self.read_json()
|
||||
return self.send_json(maria_sync(force=bool(data.get("force"))))
|
||||
if path == "/api/restart-self":
|
||||
self.send_json({"ok": True, "message": "dashboard-ul reporneste in 1s"})
|
||||
threading.Thread(target=lambda: (time.sleep(1), os._exit(0)), daemon=True).start()
|
||||
@@ -608,6 +758,50 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return self.send_json({"ok": bool(ok), "request_id": rid, "decision": decision},
|
||||
200 if ok else 404)
|
||||
|
||||
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
|
||||
dintr-un nume de unit arbitrar din request."""
|
||||
data = self.read_json()
|
||||
which = str(data.get("service") or "")
|
||||
action = str(data.get("action") or "")
|
||||
unit = MARIA_UNITS.get(which)
|
||||
if not unit:
|
||||
return self.send_json({"ok": False, "error": f"serviciu Maria necunoscut: {which}"}, 400)
|
||||
if action not in ("start", "stop", "restart"):
|
||||
return self.send_json({"ok": False, "error": f"actiune necunoscuta: {action}"}, 400)
|
||||
who = tailnet_user(self.headers) or "local"
|
||||
print(f"[actiune] {action} pe {unit}, cerut de {who}", file=sys.stderr, flush=True)
|
||||
try:
|
||||
r = _sysctl(action, unit)
|
||||
except subprocess.TimeoutExpired:
|
||||
return self.send_json({"ok": False, "error": "systemctl a depasit timpul"}, 504)
|
||||
if r.returncode != 0:
|
||||
return self.send_json({"ok": False, "error": (r.stderr or r.stdout).strip()}, 500)
|
||||
time.sleep(1.0)
|
||||
return self.send_json({"ok": True, "action": action, "service": unit_info(unit)})
|
||||
|
||||
def handle_maria_document_write(self):
|
||||
data = self.read_json()
|
||||
name = str(data.get("name") or "")
|
||||
content = data.get("content")
|
||||
if content is None:
|
||||
return self.send_json({"ok": False, "error": "lipseste 'content'"}, 400)
|
||||
try:
|
||||
maria_write_document(name, str(content))
|
||||
except (ValueError, OSError) as exc:
|
||||
return self.send_json({"ok": False, "error": str(exc)}, 400)
|
||||
return self.send_json({"ok": True, "name": name})
|
||||
|
||||
def handle_maria_document_delete(self):
|
||||
data = self.read_json()
|
||||
name = str(data.get("name") or "")
|
||||
try:
|
||||
ok = maria_delete_document(name)
|
||||
except (ValueError, OSError) as exc:
|
||||
return self.send_json({"ok": False, "error": str(exc)}, 400)
|
||||
return self.send_json({"ok": ok, "name": name}, 200 if ok else 404)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
bind = config.get("DASHBOARD_BIND") or "127.0.0.1"
|
||||
|
||||
@@ -71,6 +71,70 @@
|
||||
<pre class="log" id="log">se încarcă…</pre>
|
||||
</section>
|
||||
|
||||
<!-- ── Maria: WhatsApp + RAG ──────────────────────────────── -->
|
||||
<h2 style="margin-top:var(--space-6)">Maria — WhatsApp + RAG</h2>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<h2>Conectare WhatsApp</h2>
|
||||
<div id="mWaStatus" class="empty">se încarcă…</div>
|
||||
<div id="mWaQr"></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
|
||||
<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>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="toast"></div>
|
||||
@@ -274,10 +338,125 @@ async function refreshLogs() {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
|
||||
// ── Maria: WhatsApp + RAG ───────────────────────────────────────
|
||||
var mBusy = false;
|
||||
function mMetricsHtml(info) {
|
||||
return [
|
||||
['Stare', '<span class="pill ' + (info.active ? 'ok' : 'bad') + '">' + esc(info.state) + '/' + esc(info.sub) + '</span>'],
|
||||
['Uptime', dur(info.uptime_s)],
|
||||
['PID', info.pid || '—'],
|
||||
['Restarturi', info.restarts]
|
||||
].map(function (m) {
|
||||
return '<div class="metric"><div class="label">' + m[0] + '</div><div class="value">' + m[1] + '</div></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function mRefresh() {
|
||||
var res = await api('api/maria/status');
|
||||
if (!res.ok) return;
|
||||
var s = res.data;
|
||||
|
||||
document.getElementById('mBridgeUnit').textContent = s.bridge.unit;
|
||||
document.getElementById('mBridgeMetrics').innerHTML = mMetricsHtml(s.bridge);
|
||||
document.getElementById('mRagUnit').textContent = s.rag.unit;
|
||||
document.getElementById('mRagMetrics').innerHTML = mMetricsHtml(s.rag);
|
||||
|
||||
var wa = s.whatsapp || {};
|
||||
var waStatus = document.getElementById('mWaStatus');
|
||||
var waQr = document.getElementById('mWaQr');
|
||||
if (wa.connected) {
|
||||
waStatus.innerHTML = '<span class="pill ok">conectat</span> ca ' + esc(wa.phone || '?');
|
||||
waQr.innerHTML = '';
|
||||
} else if (wa.qr) {
|
||||
waStatus.innerHTML = '<span class="pill bad">neconectat</span> — scanează codul QR din WhatsApp → Dispozitive conectate';
|
||||
waQr.innerHTML = '<img src="' + wa.qr + '" style="width:220px;height:220px;border-radius:8px;background:#fff;padding:8px;margin-top:var(--space-4)">';
|
||||
} else {
|
||||
waStatus.innerHTML = '<span class="pill bad">' + (wa.reachable === false ? 'puntea nu rulează' : 'aștept codul QR…') + '</span>';
|
||||
waQr.innerHTML = '';
|
||||
}
|
||||
|
||||
document.getElementById('mIndexDetail').textContent =
|
||||
s.index.chunks + ' chunk-uri din ' + s.index.documents_indexed + ' documente indexate' +
|
||||
(s.index.mtime ? ' · indexat ' + new Date(s.index.mtime * 1000).toLocaleString('ro-RO') : '');
|
||||
document.getElementById('mDocCount').textContent = s.documents + ' în depozit';
|
||||
|
||||
var sync = s.sync || {};
|
||||
document.getElementById('mSyncDetail').textContent = sync.drive_remote
|
||||
? 'Drive: ' + sync.drive_remote + (sync.synced_at ? ' · ultima sincronizare ' + new Date(sync.synced_at * 1000).toLocaleString('ro-RO') : ' · încă nesincronizat')
|
||||
: 'Sincronizare Drive dezactivată (DRIVE_REMOTE nesetat) — doar upload manual';
|
||||
|
||||
var docsRes = await api('api/maria/documents');
|
||||
var docs = docsRes.ok ? (docsRes.data.documents || []) : [];
|
||||
document.getElementById('mDocs').innerHTML = docs.length ? docs.map(function (d) {
|
||||
return '<tr><td data-label="Nume" class="mono">' + esc(d.name) + '</td>' +
|
||||
'<td data-label="Mărime" class="num">' + d.size + ' B</td>' +
|
||||
'<td><button class="small danger" onclick="mDelDoc(\'' + esc(d.name) + '\')">Șterge</button></td></tr>';
|
||||
}).join('') : '<tr><td colspan="3" class="empty" data-label="">niciun document</td></tr>';
|
||||
}
|
||||
|
||||
async function mSvc(which, action) {
|
||||
if (mBusy) return;
|
||||
if (action !== 'start' && !confirm('Sigur „' + action + '” pe ' + which + '?')) return;
|
||||
mBusy = true;
|
||||
try {
|
||||
var res = await post('api/maria/service', { service: which, action: action });
|
||||
toast(res.ok ? 'Maria ' + which + ': ' + action + ' OK' : ('Eșuat: ' + (res.data.error || res.status)), res.ok ? 'ok' : 'bad');
|
||||
} finally {
|
||||
mBusy = false;
|
||||
mRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
async function mReindex() {
|
||||
var res = await post('api/maria/reindex', {});
|
||||
toast(res.ok && res.data.ok ? 'Index reconstruit' : ('Eșuat: ' + (res.data.error || res.data.stderr || res.status)), res.ok && res.data.ok ? 'ok' : 'bad');
|
||||
mRefresh();
|
||||
}
|
||||
|
||||
async function mSyncNow() {
|
||||
var res = await post('api/maria/sync', { force: true });
|
||||
toast(res.ok && res.data.ok ? 'Sincronizat' : ('Eșuat: ' + (res.data.error || res.data.stderr || res.status)), res.ok && res.data.ok ? 'ok' : 'bad');
|
||||
mRefresh();
|
||||
}
|
||||
|
||||
async function mAddDoc() {
|
||||
var name = document.getElementById('mDocName').value.trim();
|
||||
var content = document.getElementById('mDocContent').value;
|
||||
var msg = document.getElementById('mDocMsg');
|
||||
if (!name || !content) { msg.textContent = 'completează numele și conținutul'; return; }
|
||||
var res = await post('api/maria/documents', { name: name, content: content });
|
||||
if (!res.ok) { msg.textContent = 'eșuat: ' + (res.data.error || res.status); return; }
|
||||
msg.textContent = 'salvat, reindexez…';
|
||||
var ri = await post('api/maria/reindex', {});
|
||||
msg.textContent = ri.ok && ri.data.ok ? 'salvat și reindexat' : ('salvat, reindexarea a eșuat: ' + (ri.data.error || ri.data.stderr || ''));
|
||||
document.getElementById('mDocContent').value = '';
|
||||
mRefresh();
|
||||
}
|
||||
|
||||
async function mDelDoc(name) {
|
||||
if (!confirm('Ștergi ' + name + '?')) return;
|
||||
await post('api/maria/documents/delete', { name: name });
|
||||
await mReindex();
|
||||
}
|
||||
|
||||
var mLogFile = 'bridge';
|
||||
function mSetLog(which) { mLogFile = which; mRefreshLogs(); }
|
||||
async function mRefreshLogs() {
|
||||
document.getElementById('mTabBridge').className = 'small' + (mLogFile === 'bridge' ? ' primary' : '');
|
||||
document.getElementById('mTabRag').className = 'small' + (mLogFile === 'rag' ? ' primary' : '');
|
||||
var res = await api('api/maria/logs?lines=300&service=' + mLogFile);
|
||||
if (!res.ok) return;
|
||||
var el = document.getElementById('mLog');
|
||||
el.textContent = (res.data.lines || []).join('\n') || '(gol)';
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
|
||||
// ── pornire ───────────────────────────────────────────────────
|
||||
refresh(); refreshDoctor(); refreshLogs();
|
||||
mRefresh(); mRefreshLogs();
|
||||
setInterval(refresh, 5000);
|
||||
setInterval(refreshDoctor, 30000);
|
||||
setInterval(mRefresh, 4000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user