Cost showed $0 for every streamed request: the X-Cortiq-Cost-USD header is written before generation, when the cost cannot be known. The gateway now ships the final number in a closing SSE event (`cortiq_cost_usd`, alongside the token estimate when the provider sent no usage), and the console prefers it over the header. Money renders with 4 decimals, the last two in a smaller font — $0.0006 stays readable instead of collapsing to $0. Timeouts, both ends: the router default was 800 ms, silently degrading every prompt the hosted router escalates to its oracle (~10 s) — now 15 s. Provider clients had NO timeout at all (a dead OpenRouter host hung requests forever) — now a 10 s connect timeout, with no overall cap so long generations and streams are never cut. Benchmark history: per-run × and a Clear-all button. Logs view: a clear button backed by DELETE /admin/api/logs.
96 lines
2.9 KiB
JavaScript
Vendored
96 lines
2.9 KiB
JavaScript
Vendored
// Gateway log viewer — the in-memory ring served by /admin/api/logs, for
|
|
// everyone who never sees the terminal (Docker, autostart, double-clicked exe).
|
|
import { h, mount, toast } from "../ui.js";
|
|
import { t } from "../i18n.js";
|
|
import { api } from "../api.js";
|
|
|
|
export function renderLogs() {
|
|
const root = h("div");
|
|
let timer = null;
|
|
let paused = false;
|
|
let filter = "";
|
|
let level = "";
|
|
|
|
const pre = h("pre", { class: "conv-log logs-view" }, t("common.loading"));
|
|
const filterIn = h("input", { placeholder: t("logs.filterPlaceholder"), style: "max-width:260px" });
|
|
const levelSel = h(
|
|
"select",
|
|
{ style: "max-width:130px" },
|
|
h("option", { value: "" }, t("common.all")),
|
|
...["ERROR", "WARN", "INFO", "DEBUG"].map((l) => h("option", { value: l }, l))
|
|
);
|
|
const pauseBtn = h("button", { class: "btn ghost sm" }, "⏸ " + t("logs.pause"));
|
|
const copyBtn = h("button", { class: "btn ghost sm" }, "⧉ " + t("common.copy"));
|
|
const clearBtn = h("button", { class: "btn ghost sm danger" }, "🗑 " + t("dash.clear"));
|
|
clearBtn.addEventListener("click", async () => {
|
|
try {
|
|
await api.clearLogs();
|
|
lines = [];
|
|
paint();
|
|
toast(t("dash.cleared"), "ok");
|
|
} catch (e) {
|
|
toast(String(e.message || e), "err");
|
|
}
|
|
});
|
|
|
|
let lines = [];
|
|
function paint() {
|
|
let shown = lines;
|
|
if (level) shown = shown.filter((l) => l.includes(` ${level} `) || l.includes(`${level} `));
|
|
if (filter) shown = shown.filter((l) => l.toLowerCase().includes(filter));
|
|
pre.textContent = shown.length ? shown.join("\n") : t("logs.empty");
|
|
if (!paused) pre.scrollTop = pre.scrollHeight;
|
|
}
|
|
async function refresh() {
|
|
if (paused) return;
|
|
try {
|
|
lines = (await api.logs(500)).lines || [];
|
|
paint();
|
|
} catch {
|
|
/* transient */
|
|
}
|
|
}
|
|
filterIn.addEventListener("input", () => {
|
|
filter = filterIn.value.trim().toLowerCase();
|
|
paint();
|
|
});
|
|
levelSel.addEventListener("change", () => {
|
|
level = levelSel.value;
|
|
paint();
|
|
});
|
|
pauseBtn.addEventListener("click", () => {
|
|
paused = !paused;
|
|
pauseBtn.textContent = (paused ? "▶ " : "⏸ ") + t(paused ? "logs.resume" : "logs.pause");
|
|
if (!paused) refresh();
|
|
});
|
|
copyBtn.addEventListener("click", async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(pre.textContent);
|
|
toast(t("common.copied"), "ok");
|
|
} catch (e) {
|
|
toast(String(e.message || e), "err");
|
|
}
|
|
});
|
|
|
|
timer = setInterval(() => {
|
|
if (!root.isConnected) {
|
|
clearInterval(timer);
|
|
return;
|
|
}
|
|
refresh();
|
|
}, 2000);
|
|
refresh();
|
|
|
|
mount(
|
|
root,
|
|
h("div", { class: "view-head" }, h("h2", {}, t("nav.logs")), h("div", { class: "muted" }, t("logs.subtitle"))),
|
|
h(
|
|
"div",
|
|
{ class: "card" },
|
|
h("div", { class: "flex", style: "gap:8px; flex-wrap:wrap; margin-bottom:10px" }, filterIn, levelSel, pauseBtn, copyBtn, clearBtn),
|
|
pre
|
|
)
|
|
);
|
|
return root;
|
|
}
|