Fifteen changes aimed at the first day of real use: - Import and media jobs persist to <data>/jobs-*.json and reload on start (running ones settle as errors); the Register button no longer vanishes because the gateway restarted. - Docker ships the cortiq binary (built in the builder stage) — local .cmf chat models and image/video generation now work in containers, where auto-install never could (no cargo in the runtime image). libvulkan1 added for optional GPU offload. - Open admin console on 0.0.0.0 without a token now warns in the log and shows a dismissible banner — the VPS footgun is at least labeled. - "Restart gateway" button in Settings: stops managed model servers, spawns a fresh copy (bind retried while the port frees), Docker relies on the container restart policy instead. - Dashboard "System" panel: CPU sparkline + load, RAM (with the gateway's own RSS), free disk at the models dir — sampled by sysinfo every 2 s. - Featured model cards warn "not enough disk" / "may not fit in RAM" before the download, from the same live snapshot. - Log viewer in the sidebar: a 500-line in-memory ring fed by a second ANSI-free tracing layer; filter, level, pause, copy. - Update check: crates.io polled twice a day, the console shows an "update available" notice. - Media generations queue up (with position shown) instead of being rejected; progress gained an honest ETA from measured throughput. - Generation gallery under the image/video tabs: lazy PPM thumbnails, click to reopen with seed/steps, delete. - Playground chat is a real dialogue now: history is resent, bubbles, Clear, Cmd/Ctrl+Enter. - "Connect your app" snippets (curl / Python / JS) on the keys page, with the real key inlined right after creation. - One-click benchmark on the Models page via `cortiq bench --json`. - Config export now downloads the actual gateway.toml (comments intact); import accepts TOML (validated, hot-reloaded) and the legacy JSON. - Streaming token counts are exact when the model server provides usage (cortiq-server patch, shipped separately); the gateway fallback now counts content chunks (1/token on cortiq) instead of chars/4. Wizard's Finish lands on the Playground. 37 new strings in all 7 languages.
85 lines
2.6 KiB
JavaScript
Vendored
85 lines
2.6 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"));
|
|
|
|
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),
|
|
pre
|
|
)
|
|
);
|
|
return root;
|
|
}
|