Version 1.2.2. Added usage overview. Shows token used and cost in $.
This commit is contained in:
@@ -215,6 +215,7 @@ function _initPage(url) {
|
||||
if (path === "/" || path === "") { initChat(); return; }
|
||||
if (path === "/agents") { initAgents(); return; }
|
||||
if (path.startsWith("/agents/")) { initAgentDetail(); return; }
|
||||
if (path === "/usage") { initUsage(); return; }
|
||||
if (path === "/audit") { initAudit(); return; }
|
||||
if (path === "/monitors") { initMonitors(); return; }
|
||||
if (path === "/models") { initModels(); return; }
|
||||
@@ -1185,6 +1186,122 @@ function respondConfirm(approved) {
|
||||
document.getElementById("confirm-modal").classList.add("hidden");
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════════
|
||||
USAGE PAGE
|
||||
══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
let _usageRange = "7d";
|
||||
|
||||
function setUsageRange(range) {
|
||||
_usageRange = range;
|
||||
["today","7d","30d","all"].forEach(r => {
|
||||
const btn = document.getElementById("usage-range-" + r);
|
||||
if (!btn) return;
|
||||
const active = r === range;
|
||||
btn.style.background = active ? "var(--accent)" : "";
|
||||
btn.style.color = active ? "#fff" : "";
|
||||
btn.style.borderColor = active ? "var(--accent)" : "";
|
||||
});
|
||||
loadUsage();
|
||||
}
|
||||
|
||||
function _fmtTokens(n) {
|
||||
if (n === null || n === undefined) return "—";
|
||||
if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + "M";
|
||||
if (n >= 1_000) return (n / 1_000).toFixed(1) + "k";
|
||||
return String(n);
|
||||
}
|
||||
|
||||
function _fmtCost(c) {
|
||||
if (c === null || c === undefined) return "—";
|
||||
if (c < 0.001) return "< $0.001";
|
||||
return "$" + c.toFixed(4);
|
||||
}
|
||||
|
||||
async function loadUsage() {
|
||||
const tbody = document.getElementById("usage-tbody");
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:var(--text-dim);padding:32px">Loading…</td></tr>';
|
||||
|
||||
const resp = await fetch("/api/usage?since=" + encodeURIComponent(_usageRange));
|
||||
if (!resp.ok) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:var(--danger)">Failed to load usage data</td></tr>';
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
const s = data.summary;
|
||||
|
||||
// Update summary cards
|
||||
const set = (id, v) => { const el = document.getElementById(id); if (el) el.textContent = v; };
|
||||
set("u-total-runs", s.runs.toLocaleString());
|
||||
set("u-input-tokens", _fmtTokens(s.input_tokens));
|
||||
set("u-output-tokens", _fmtTokens(s.output_tokens));
|
||||
set("u-total-tokens", _fmtTokens(s.input_tokens + s.output_tokens));
|
||||
set("u-cost", _fmtCost(s.cost_usd));
|
||||
|
||||
// Find max tokens for bar scaling
|
||||
const agents = data.by_agent;
|
||||
const maxTokens = agents.reduce((m, a) => Math.max(m, a.input_tokens + a.output_tokens), 1);
|
||||
let anyNullCost = false;
|
||||
|
||||
if (!agents.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:var(--text-dim);padding:32px">No runs found for this period</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = agents.map(a => {
|
||||
const total = a.input_tokens + a.output_tokens;
|
||||
const pct = maxTokens > 0 ? Math.round((total / maxTokens) * 100) : 0;
|
||||
const costStr = _fmtCost(a.cost_usd);
|
||||
if (a.cost_usd === null) anyNullCost = true;
|
||||
const modelShort = a.model ? a.model.replace(/^(anthropic|openrouter|openai):/, "") : "—";
|
||||
const modelTitle = a.model || "";
|
||||
return `<tr>
|
||||
<td><a href="/agents/${a.agent_id}" style="color:var(--accent);text-decoration:none">${esc(a.agent_name)}</a></td>
|
||||
<td style="font-size:12px;color:var(--text-dim)" title="${esc(modelTitle)}">${esc(modelShort)}</td>
|
||||
<td style="text-align:right">${a.runs.toLocaleString()}</td>
|
||||
<td style="text-align:right;font-size:13px">${_fmtTokens(a.input_tokens)}</td>
|
||||
<td style="text-align:right;font-size:13px">${_fmtTokens(a.output_tokens)}</td>
|
||||
<td style="text-align:right;font-size:13px">${_fmtTokens(total)}</td>
|
||||
<td style="text-align:right;font-size:13px;font-weight:${a.cost_usd !== null ? "600" : "400"}">${costStr}</td>
|
||||
<td>
|
||||
<div style="background:var(--bg2);border-radius:3px;height:6px;overflow:hidden">
|
||||
<div style="background:var(--accent);height:100%;width:${pct}%;transition:width 0.3s"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join("");
|
||||
|
||||
// Chat section
|
||||
const chatTbody = document.getElementById("usage-chat-tbody");
|
||||
if (chatTbody) {
|
||||
const c = data.chat;
|
||||
if (!c || c.sessions === 0) {
|
||||
chatTbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--text-dim);padding:24px">No chat sessions in this period</td></tr>';
|
||||
} else {
|
||||
const chatTotal = c.input_tokens + c.output_tokens;
|
||||
if (c.cost_usd === null) anyNullCost = true;
|
||||
chatTbody.innerHTML = `<tr>
|
||||
<td>Interactive chat</td>
|
||||
<td style="text-align:right">${c.sessions.toLocaleString()}</td>
|
||||
<td style="text-align:right;font-size:13px">${_fmtTokens(c.input_tokens)}</td>
|
||||
<td style="text-align:right;font-size:13px">${_fmtTokens(c.output_tokens)}</td>
|
||||
<td style="text-align:right;font-size:13px">${_fmtTokens(chatTotal)}</td>
|
||||
<td style="text-align:right;font-size:13px;font-weight:${c.cost_usd !== null ? "600" : "400"}">${_fmtCost(c.cost_usd)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
const note = document.getElementById("usage-no-cost-note");
|
||||
if (note) note.style.display = anyNullCost ? "" : "none";
|
||||
}
|
||||
|
||||
function initUsage() {
|
||||
if (!document.getElementById("usage-container")) return;
|
||||
loadUsage();
|
||||
}
|
||||
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════════
|
||||
AUDIT PAGE
|
||||
══════════════════════════════════════════════════════════════════════════ */
|
||||
@@ -1460,6 +1577,7 @@ function switchUserTab(name) {
|
||||
if (name === "brain") { loadBrainKey("ubrain-mcp-key", "ubrain-mcp-cmd"); loadBrainAutoApprove(); }
|
||||
if (name === "mfa") { loadTheme(); loadMyProfile(); loadMfaStatus(); loadDataFolder(); }
|
||||
if (name === "webhooks") { loadMyWebhooks(); loadMyWebhookTargets(); }
|
||||
if (name === "browser") { loadMyBrowserTrusted(); }
|
||||
if (name === "pushover") { loadMyPushover(); }
|
||||
}
|
||||
|
||||
@@ -2426,6 +2544,11 @@ function initUserSettings() {
|
||||
loadMyProviderKeys();
|
||||
loadMyPersonality();
|
||||
loadMyMcpServers();
|
||||
|
||||
document.getElementById("my-browser-trusted-form")?.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
await addBrowserTrusted("my-bt-domain", "my-bt-note");
|
||||
});
|
||||
}
|
||||
|
||||
function initSettings() {
|
||||
@@ -2452,6 +2575,7 @@ function initSettings() {
|
||||
loadEmailWhitelist();
|
||||
loadWebWhitelist();
|
||||
loadFilesystemWhitelist();
|
||||
loadBrowserTrusted();
|
||||
reloadCredList();
|
||||
loadInboxStatus();
|
||||
loadInboxTriggers();
|
||||
@@ -2530,6 +2654,11 @@ function initSettings() {
|
||||
await addFilesystemPath();
|
||||
});
|
||||
|
||||
document.getElementById("browser-trusted-form")?.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
await addBrowserTrusted("bt-domain", "bt-note");
|
||||
});
|
||||
|
||||
document.getElementById("brain-capture-form")?.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
const content = document.getElementById("brain-capture-text").value.trim();
|
||||
@@ -3029,6 +3158,75 @@ const _DEDICATED_CRED_KEYS = new Set([
|
||||
// These are managed by Inbox / Telegram / Brain / Security / Branding tabs
|
||||
]);
|
||||
|
||||
/* ── Browser trusted domains ─────────────────────────────────────────────── */
|
||||
|
||||
function _renderBrowserTrustedTable(rows, tbodyId) {
|
||||
const tbody = document.getElementById(tbodyId);
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = "";
|
||||
if (!rows.length) {
|
||||
tbody.innerHTML = "<tr><td colspan='3' style='text-align:center;color:var(--text-dim)'>No trusted domains</td></tr>";
|
||||
return;
|
||||
}
|
||||
for (const row of rows) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td><code>${esc(row.domain)}</code></td>
|
||||
<td style="color:var(--text-dim)">${esc(row.note || "")}</td>
|
||||
<td><button class="btn btn-danger" style="padding:4px 10px;font-size:12px"
|
||||
onclick="removeBrowserTrusted('${esc(row.domain)}')">Remove</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBrowserTrusted() {
|
||||
const r = await fetch("/api/my/browser-trusted");
|
||||
if (!r.ok) return;
|
||||
_renderBrowserTrustedTable(await r.json(), "browser-trusted-list");
|
||||
}
|
||||
|
||||
async function loadMyBrowserTrusted() {
|
||||
const r = await fetch("/api/my/browser-trusted");
|
||||
if (!r.ok) return;
|
||||
_renderBrowserTrustedTable(await r.json(), "my-browser-trusted-list");
|
||||
}
|
||||
|
||||
async function addBrowserTrusted(domainInputId, noteInputId) {
|
||||
const domain = document.getElementById(domainInputId)?.value.trim();
|
||||
const note = document.getElementById(noteInputId)?.value.trim();
|
||||
if (!domain) return;
|
||||
const r = await fetch("/api/my/browser-trusted", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ domain, note }),
|
||||
});
|
||||
if (r.ok) {
|
||||
document.getElementById(domainInputId).value = "";
|
||||
document.getElementById(noteInputId).value = "";
|
||||
showFlash("Added ✓");
|
||||
loadBrowserTrusted();
|
||||
loadMyBrowserTrusted();
|
||||
} else {
|
||||
const d = await r.json().catch(() => ({}));
|
||||
alert(d.detail || "Error adding domain");
|
||||
}
|
||||
}
|
||||
|
||||
async function removeBrowserTrusted(domain) {
|
||||
if (!confirm(`Remove "${domain}" from trusted domains?`)) return;
|
||||
const r = await fetch(`/api/my/browser-trusted/${encodeURIComponent(domain)}`, { method: "DELETE" });
|
||||
if (r.ok) {
|
||||
showFlash("Removed ✓");
|
||||
loadBrowserTrusted();
|
||||
loadMyBrowserTrusted();
|
||||
} else {
|
||||
showFlash("Error removing domain");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── end browser trusted domains ─────────────────────────────────────────── */
|
||||
|
||||
async function reloadCredList() {
|
||||
const r = await fetch("/api/credentials");
|
||||
if (!r.ok) return;
|
||||
|
||||
@@ -609,3 +609,8 @@ tr:hover td { background: var(--bg2); }
|
||||
.pm-btn:last-child { border-right: none; }
|
||||
.pm-btn.active { background: var(--accent); color: #fff; }
|
||||
.pm-btn:hover:not(.active) { background: var(--bg3); color: var(--text); }
|
||||
|
||||
/* ── Usage page stat cards ────────────────────────────────────────────────── */
|
||||
.stat-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px 20px; }
|
||||
.stat-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-dim); margin-bottom: 6px; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; color: var(--text); font-family: var(--mono); }
|
||||
|
||||
Reference in New Issue
Block a user