Skip to content

Commit e6e40a5

Browse files
committed
feat(console): improve observer monitor formatting and readability
- Parse ASCII table output into proper HTML tables with headers and rows - Add component health indicators with visual status dots (ok/error) - Create auto-refresh toggle for observer monitoring (10s interval) - Render system status as key-value grid instead of raw JSON - Organize components as cards: Queue, VectorDB, VLM, Retrieval, Locks - Add styling for monitor dashboard (cards, tables, health dots) Contributed by the engineering team at Homesage.ai
1 parent d56d7d4 commit e6e40a5

File tree

3 files changed

+224
-9
lines changed

3 files changed

+224
-9
lines changed

openviking/console/static/app.js

Lines changed: 119 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ const elements = {
111111
systemBtn: document.getElementById("systemBtn"),
112112
observerBtn: document.getElementById("observerBtn"),
113113
monitorResults: document.getElementById("monitorResults"),
114+
monitorDashboard: document.getElementById("monitorDashboard"),
115+
monitorRefreshBtn: document.getElementById("monitorRefreshBtn"),
114116
navToggleBtn: document.getElementById("navToggleBtn"),
115117
resultToggleBtn: document.getElementById("resultToggleBtn"),
116118
clearOutputBtn: document.getElementById("clearOutputBtn"),
@@ -2431,31 +2433,139 @@ function bindTenants() {
24312433
});
24322434
}
24332435

2436+
function parseAsciiTable(text) {
2437+
if (!text || typeof text !== "string") return null;
2438+
const lines = text.split("\n").filter(l => l.trim());
2439+
const dataLines = lines.filter(l => !l.match(/^[+\-]+$/));
2440+
if (dataLines.length < 2) return null;
2441+
const splitRow = row =>
2442+
row.split("|").slice(1, -1).map(c => c.trim());
2443+
const headers = splitRow(dataLines[0]);
2444+
const rows = dataLines.slice(1).map(splitRow);
2445+
return { headers, rows };
2446+
}
2447+
2448+
function renderMonitorTable(parsed) {
2449+
if (!parsed) return "";
2450+
let html = "<table><thead><tr>";
2451+
for (const h of parsed.headers) html += `<th>${h}</th>`;
2452+
html += "</tr></thead><tbody>";
2453+
for (const row of parsed.rows) {
2454+
const isTotal = row.some(c => /^TOTAL$/i.test(c));
2455+
html += `<tr${isTotal ? ' class="total-row"' : ""}>`;
2456+
for (const cell of row) html += `<td>${cell}</td>`;
2457+
html += "</tr>";
2458+
}
2459+
html += "</tbody></table>";
2460+
return html;
2461+
}
2462+
2463+
function renderComponentCard(name, comp) {
2464+
const healthy = comp.is_healthy !== false;
2465+
const dotClass = healthy ? "ok" : "error";
2466+
const friendlyNames = {
2467+
queue: "Processing Queue",
2468+
vikingdb: "Vector Database",
2469+
vlm: "Language Model",
2470+
lock: "Active Locks",
2471+
retrieval: "Retrieval Stats",
2472+
};
2473+
const title = friendlyNames[name] || name;
2474+
let bodyHtml = "";
2475+
const statusText = comp.status || "";
2476+
const tables = statusText.split("\n\n").filter(Boolean);
2477+
for (const tableText of tables) {
2478+
const parsed = parseAsciiTable(tableText);
2479+
if (parsed) {
2480+
bodyHtml += renderMonitorTable(parsed);
2481+
} else {
2482+
bodyHtml += `<div class="plain-text">${tableText}</div>`;
2483+
}
2484+
}
2485+
return `<div class="monitor-card">
2486+
<div class="monitor-card-header">
2487+
<span class="health-dot ${dotClass}"></span>
2488+
<h3>${title}</h3>
2489+
</div>
2490+
${bodyHtml}
2491+
</div>`;
2492+
}
2493+
2494+
function renderSystemStatus(result) {
2495+
const dashboard = elements.monitorDashboard;
2496+
if (!result) { dashboard.innerHTML = ""; return; }
2497+
let html = `<div class="monitor-card">
2498+
<div class="monitor-card-header">
2499+
<span class="health-dot ${result.initialized ? "ok" : "error"}"></span>
2500+
<h3>System</h3>
2501+
</div>
2502+
<div class="kv-grid">`;
2503+
for (const [k, v] of Object.entries(result)) {
2504+
const display = typeof v === "string" ? v : JSON.stringify(v);
2505+
html += `<span class="kv-label">${k}</span><span class="kv-value">${display}</span>`;
2506+
}
2507+
html += "</div></div>";
2508+
dashboard.innerHTML = html;
2509+
}
2510+
2511+
function renderObserverDashboard(result) {
2512+
const dashboard = elements.monitorDashboard;
2513+
if (!result?.components) { dashboard.innerHTML = ""; return; }
2514+
const healthSummary = Object.entries(result.components).map(([name, comp]) => {
2515+
const healthy = comp.is_healthy !== false;
2516+
return `<span class="health-dot ${healthy ? "ok" : "error"}"></span> ${name}`;
2517+
}).join("&nbsp;&nbsp;&nbsp;");
2518+
let html = `<div style="margin-bottom:12px;display:flex;align-items:center;gap:12px;font-size:13px;color:var(--muted)">${healthSummary}</div>`;
2519+
const order = ["queue", "vikingdb", "vlm", "retrieval", "lock"];
2520+
const sorted = order.filter(n => result.components[n]);
2521+
for (const miss of Object.keys(result.components)) {
2522+
if (!sorted.includes(miss)) sorted.push(miss);
2523+
}
2524+
for (const name of sorted) {
2525+
html += renderComponentCard(name, result.components[name]);
2526+
}
2527+
dashboard.innerHTML = html;
2528+
}
2529+
2530+
let monitorRefreshInterval = null;
2531+
24342532
function bindMonitor() {
24352533
elements.systemBtn.addEventListener("click", async () => {
24362534
try {
24372535
const payload = await callConsole("/ov/system/status", { method: "GET" });
2438-
const rows = Object.entries(payload.result || {}).map(([key, value]) => ({
2439-
label: `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`,
2440-
}));
2441-
renderList(elements.monitorResults, rows);
2536+
elements.monitorResults.innerHTML = "";
2537+
renderSystemStatus(payload.result);
24422538
setOutput(payload);
24432539
} catch (error) {
24442540
setOutput(error.message);
24452541
}
24462542
});
24472543

2448-
elements.observerBtn.addEventListener("click", async () => {
2544+
async function loadObserver() {
24492545
try {
24502546
const payload = await callConsole("/ov/observer/system", { method: "GET" });
2451-
const rows = Object.entries(payload.result?.components || {}).map(([name, value]) => ({
2452-
label: `${name}: ${value?.status || JSON.stringify(value)}`,
2453-
}));
2454-
renderList(elements.monitorResults, rows);
2547+
elements.monitorResults.innerHTML = "";
2548+
renderObserverDashboard(payload.result);
24552549
setOutput(payload);
24562550
} catch (error) {
24572551
setOutput(error.message);
24582552
}
2553+
}
2554+
2555+
elements.observerBtn.addEventListener("click", loadObserver);
2556+
2557+
elements.monitorRefreshBtn.addEventListener("click", () => {
2558+
if (monitorRefreshInterval) {
2559+
clearInterval(monitorRefreshInterval);
2560+
monitorRefreshInterval = null;
2561+
elements.monitorRefreshBtn.textContent = "Auto-refresh: OFF";
2562+
elements.monitorRefreshBtn.classList.remove("active");
2563+
} else {
2564+
loadObserver();
2565+
monitorRefreshInterval = setInterval(loadObserver, 10000);
2566+
elements.monitorRefreshBtn.textContent = "Auto-refresh: ON";
2567+
elements.monitorRefreshBtn.classList.add("active");
2568+
}
24592569
});
24602570
}
24612571

openviking/console/static/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,9 @@ <h2>Monitor</h2>
312312
<div class="row">
313313
<button id="systemBtn">System Status</button>
314314
<button id="observerBtn">Observer(System)</button>
315+
<button id="monitorRefreshBtn" title="Auto-refresh every 10s">Auto-refresh: OFF</button>
315316
</div>
317+
<div id="monitorDashboard" class="monitor-dashboard"></div>
316318
<ul id="monitorResults" class="list"></ul>
317319
</section>
318320

openviking/console/static/styles.css

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1479,6 +1479,109 @@ body.dragging-output * {
14791479
}
14801480
}
14811481

1482+
/* Monitor Dashboard */
1483+
.monitor-dashboard {
1484+
display: flex;
1485+
flex-direction: column;
1486+
gap: 16px;
1487+
margin-top: 12px;
1488+
}
1489+
1490+
.monitor-card {
1491+
background: var(--card);
1492+
border: 1px solid var(--border);
1493+
border-radius: 8px;
1494+
padding: 16px;
1495+
overflow-x: auto;
1496+
}
1497+
1498+
.monitor-card-header {
1499+
display: flex;
1500+
align-items: center;
1501+
gap: 10px;
1502+
margin-bottom: 12px;
1503+
}
1504+
1505+
.monitor-card-header h3 {
1506+
margin: 0;
1507+
font-size: 14px;
1508+
font-weight: 600;
1509+
text-transform: uppercase;
1510+
letter-spacing: 0.5px;
1511+
color: var(--text);
1512+
}
1513+
1514+
.health-dot {
1515+
width: 10px;
1516+
height: 10px;
1517+
border-radius: 50%;
1518+
flex-shrink: 0;
1519+
}
1520+
1521+
.health-dot.ok { background: var(--ok, #30c482); }
1522+
.health-dot.error { background: var(--danger, #ff5c5c); }
1523+
1524+
.monitor-card table {
1525+
width: 100%;
1526+
border-collapse: collapse;
1527+
font-family: "JetBrains Mono", monospace;
1528+
font-size: 12px;
1529+
}
1530+
1531+
.monitor-card th {
1532+
text-align: left;
1533+
padding: 6px 12px;
1534+
color: var(--muted);
1535+
font-weight: 500;
1536+
border-bottom: 1px solid var(--border);
1537+
white-space: nowrap;
1538+
}
1539+
1540+
.monitor-card td {
1541+
padding: 6px 12px;
1542+
color: var(--text);
1543+
border-bottom: 1px solid var(--border);
1544+
white-space: nowrap;
1545+
}
1546+
1547+
.monitor-card tr:last-child td {
1548+
border-bottom: none;
1549+
}
1550+
1551+
.monitor-card tr.total-row td {
1552+
font-weight: 600;
1553+
color: var(--text-strong);
1554+
border-top: 1px solid var(--border-strong);
1555+
}
1556+
1557+
.monitor-card .plain-text {
1558+
color: var(--muted);
1559+
font-family: "JetBrains Mono", monospace;
1560+
font-size: 12px;
1561+
}
1562+
1563+
.monitor-card .kv-grid {
1564+
display: grid;
1565+
grid-template-columns: auto 1fr;
1566+
gap: 4px 16px;
1567+
}
1568+
1569+
.monitor-card .kv-label {
1570+
color: var(--muted);
1571+
font-size: 12px;
1572+
}
1573+
1574+
.monitor-card .kv-value {
1575+
color: var(--text);
1576+
font-size: 12px;
1577+
font-family: "JetBrains Mono", monospace;
1578+
}
1579+
1580+
#monitorRefreshBtn.active {
1581+
background: var(--ok, #30c482);
1582+
color: #fff;
1583+
}
1584+
14821585
@media (prefers-reduced-motion: reduce) {
14831586
*,
14841587
*::before,

0 commit comments

Comments
 (0)