Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 159 additions & 9 deletions openviking/console/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ const elements = {
systemBtn: document.getElementById("systemBtn"),
observerBtn: document.getElementById("observerBtn"),
monitorResults: document.getElementById("monitorResults"),
monitorDashboard: document.getElementById("monitorDashboard"),
monitorRefreshBtn: document.getElementById("monitorRefreshBtn"),
navToggleBtn: document.getElementById("navToggleBtn"),
resultToggleBtn: document.getElementById("resultToggleBtn"),
clearOutputBtn: document.getElementById("clearOutputBtn"),
Expand Down Expand Up @@ -2431,31 +2433,179 @@ function bindTenants() {
});
}

function parseAsciiTable(text) {
if (!text || typeof text !== "string") return null;
const lines = text.split("\n").filter(l => l.trim());
const dataLines = lines.filter(l => !l.match(/^[+\-]+$/));
if (dataLines.length < 2) return null;
const splitRow = row =>
row.split("|").slice(1, -1).map(c => c.trim());
const headers = splitRow(dataLines[0]);
const rows = dataLines.slice(1).map(splitRow);
return { headers, rows };
}

function renderMonitorTable(parsed) {
if (!parsed) return document.createDocumentFragment();
const table = document.createElement("table");
const thead = table.createTHead();
const headerRow = thead.insertRow();
for (const h of parsed.headers) {
const th = document.createElement("th");
th.textContent = h;
headerRow.appendChild(th);
}
const tbody = table.createTBody();
for (const row of parsed.rows) {
const tr = tbody.insertRow();
const isTotal = row.some(c => /^TOTAL$/i.test(c));
if (isTotal) tr.className = "total-row";
for (const cell of row) {
const td = tr.insertCell();
td.textContent = cell;
}
}
return table;
}

function renderComponentCard(name, comp) {
const healthy = comp.is_healthy !== false;
const friendlyNames = {
queue: "Processing Queue",
vikingdb: "Vector Database",
vlm: "Language Model",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bug] (blocking) This code assumes the backend component key is vlm, but ObserverService.system() actually returns models (queue, vikingdb, models, lock, retrieval). As a result, this card keeps the raw models label and also falls out of the intended order because both friendlyNames and the order array are keyed on vlm. Please align the frontend mapping with the actual response contract.

lock: "Active Locks",
retrieval: "Retrieval Stats",
};

const card = document.createElement("div");
card.className = "monitor-card";

const header = document.createElement("div");
header.className = "monitor-card-header";
const dot = document.createElement("span");
dot.className = `health-dot ${healthy ? "ok" : "error"}`;
const h3 = document.createElement("h3");
h3.textContent = friendlyNames[name] || name;
header.appendChild(dot);
header.appendChild(h3);
card.appendChild(header);

const statusText = comp.status || "";
const tables = statusText.split("\n\n").filter(Boolean);
for (const tableText of tables) {
const parsed = parseAsciiTable(tableText);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bug] (blocking) parseAsciiTable() assumes every comp.status block is a plain tabulate table, but ModelsObserver.get_status_table() prefixes each table with section headers like VLM Models: before the ASCII table. In that case dataLines[0] becomes the section header, splitRow() returns [], and the real header row is rendered as body data. The Language Model card therefore does not render as the formatted table described in this PR. Please strip leading non-table lines or parse section headers separately before calling renderMonitorTable().

if (parsed) {
card.appendChild(renderMonitorTable(parsed));
} else {
const plain = document.createElement("div");
plain.className = "plain-text";
plain.textContent = tableText;
card.appendChild(plain);
}
}
return card;
}

function renderSystemStatus(result) {
const dashboard = elements.monitorDashboard;
dashboard.replaceChildren();
if (!result) return;

const card = document.createElement("div");
card.className = "monitor-card";

const header = document.createElement("div");
header.className = "monitor-card-header";
const dot = document.createElement("span");
dot.className = `health-dot ${result.initialized ? "ok" : "error"}`;
const h3 = document.createElement("h3");
h3.textContent = "System";
header.appendChild(dot);
header.appendChild(h3);
card.appendChild(header);

const grid = document.createElement("div");
grid.className = "kv-grid";
for (const [k, v] of Object.entries(result)) {
const label = document.createElement("span");
label.className = "kv-label";
label.textContent = k;
const value = document.createElement("span");
value.className = "kv-value";
value.textContent = typeof v === "string" ? v : JSON.stringify(v);
grid.appendChild(label);
grid.appendChild(value);
}
card.appendChild(grid);
dashboard.appendChild(card);
}

function renderObserverDashboard(result) {
const dashboard = elements.monitorDashboard;
dashboard.replaceChildren();
if (!result?.components) return;

const summary = document.createElement("div");
summary.style.cssText = "margin-bottom:12px;display:flex;align-items:center;gap:12px;font-size:13px;color:var(--muted)";
for (const [name, comp] of Object.entries(result.components)) {
const dot = document.createElement("span");
dot.className = `health-dot ${comp.is_healthy !== false ? "ok" : "error"}`;
const label = document.createTextNode(` ${name} `);
summary.appendChild(dot);
summary.appendChild(label);
}
dashboard.appendChild(summary);

const order = ["queue", "vikingdb", "vlm", "retrieval", "lock"];
const sorted = order.filter(n => result.components[n]);
for (const name of Object.keys(result.components)) {
if (!sorted.includes(name)) sorted.push(name);
}
for (const name of sorted) {
dashboard.appendChild(renderComponentCard(name, result.components[name]));
}
}

let monitorRefreshInterval = null;

function bindMonitor() {
elements.systemBtn.addEventListener("click", async () => {
try {
const payload = await callConsole("/ov/system/status", { method: "GET" });
const rows = Object.entries(payload.result || {}).map(([key, value]) => ({
label: `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`,
}));
renderList(elements.monitorResults, rows);
elements.monitorResults.innerHTML = "";
renderSystemStatus(payload.result);
setOutput(payload);
} catch (error) {
setOutput(error.message);
}
});

elements.observerBtn.addEventListener("click", async () => {
async function loadObserver() {
try {
const payload = await callConsole("/ov/observer/system", { method: "GET" });
const rows = Object.entries(payload.result?.components || {}).map(([name, value]) => ({
label: `${name}: ${value?.status || JSON.stringify(value)}`,
}));
renderList(elements.monitorResults, rows);
elements.monitorResults.innerHTML = "";
renderObserverDashboard(payload.result);
setOutput(payload);
} catch (error) {
setOutput(error.message);
}
}

elements.observerBtn.addEventListener("click", loadObserver);

elements.monitorRefreshBtn.addEventListener("click", () => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bug] (blocking) Once auto-refresh is enabled, nothing turns it off when the user leaves the Monitor panel or switches to the System Status view. setInterval(loadObserver, 10000) keeps firing in the background and loadObserver() keeps calling setOutput(payload), so the shared Result pane in other panels gets overwritten every 10 seconds. This is a behavior regression introduced by the new feature. Please clear the interval when leaving the Monitor panel and when switching away from observer data.

if (monitorRefreshInterval) {
clearInterval(monitorRefreshInterval);
monitorRefreshInterval = null;
elements.monitorRefreshBtn.textContent = "Auto-refresh: OFF";
elements.monitorRefreshBtn.classList.remove("active");
} else {
loadObserver();
monitorRefreshInterval = setInterval(loadObserver, 10000);
elements.monitorRefreshBtn.textContent = "Auto-refresh: ON";
elements.monitorRefreshBtn.classList.add("active");
}
});
}

Expand Down
2 changes: 2 additions & 0 deletions openviking/console/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,9 @@ <h2>Monitor</h2>
<div class="row">
<button id="systemBtn">System Status</button>
<button id="observerBtn">Observer(System)</button>
<button id="monitorRefreshBtn" title="Auto-refresh every 10s">Auto-refresh: OFF</button>
</div>
<div id="monitorDashboard" class="monitor-dashboard"></div>
<ul id="monitorResults" class="list"></ul>
</section>

Expand Down
103 changes: 103 additions & 0 deletions openviking/console/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1479,6 +1479,109 @@ body.dragging-output * {
}
}

/* Monitor Dashboard */
.monitor-dashboard {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 12px;
}

.monitor-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
overflow-x: auto;
}

.monitor-card-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}

.monitor-card-header h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text);
}

.health-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}

.health-dot.ok { background: var(--ok, #30c482); }
.health-dot.error { background: var(--danger, #ff5c5c); }

.monitor-card table {
width: 100%;
border-collapse: collapse;
font-family: "JetBrains Mono", monospace;
font-size: 12px;
}

.monitor-card th {
text-align: left;
padding: 6px 12px;
color: var(--muted);
font-weight: 500;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}

.monitor-card td {
padding: 6px 12px;
color: var(--text);
border-bottom: 1px solid var(--border);
white-space: nowrap;
}

.monitor-card tr:last-child td {
border-bottom: none;
}

.monitor-card tr.total-row td {
font-weight: 600;
color: var(--text-strong);
border-top: 1px solid var(--border-strong);
}

.monitor-card .plain-text {
color: var(--muted);
font-family: "JetBrains Mono", monospace;
font-size: 12px;
}

.monitor-card .kv-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 16px;
}

.monitor-card .kv-label {
color: var(--muted);
font-size: 12px;
}

.monitor-card .kv-value {
color: var(--text);
font-size: 12px;
font-family: "JetBrains Mono", monospace;
}

#monitorRefreshBtn.active {
background: var(--ok, #30c482);
color: #fff;
}

@media (prefers-reduced-motion: reduce) {
*,
*::before,
Expand Down