feat: ops & UX batch — jobs survive restarts, logs/system in the console, media queue, gallery, chat, exact stream tokens
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.
This commit is contained in:
Vendored
+46
-10
@@ -1,7 +1,7 @@
|
||||
// Settings — router/route/breaker/log/telemetry/stats + export/import config.
|
||||
import { h, mount, toast } from "../ui.js";
|
||||
import { h, mount, toast, confirmDialog } from "../ui.js";
|
||||
import { t } from "../i18n.js";
|
||||
import { api } from "../api.js";
|
||||
import { api, getToken, postRaw } from "../api.js";
|
||||
import { appState, SITE_URL } from "../app.js";
|
||||
|
||||
// A small "?" badge with a hover/focus tooltip explaining a setting.
|
||||
@@ -480,22 +480,33 @@ export async function renderSettings() {
|
||||
});
|
||||
|
||||
async function exportConfig() {
|
||||
const cfg = await api.getConfig();
|
||||
const blob = new Blob([JSON.stringify(cfg, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = h("a", { href: url, download: "gateway-config.json" });
|
||||
// the real gateway.toml from disk — comments and all
|
||||
const tok = getToken();
|
||||
const headers = {};
|
||||
if (tok) headers["Authorization"] = "Bearer " + tok;
|
||||
const r = await fetch("/admin/api/config/export", { headers });
|
||||
if (!r.ok) {
|
||||
toast(await r.text(), "bad");
|
||||
return;
|
||||
}
|
||||
const url = URL.createObjectURL(await r.blob());
|
||||
const a = h("a", { href: url, download: "gateway.toml" });
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
const fileIn = h("input", { type: "file", accept: ".json,application/json", style: "display:none" });
|
||||
const fileIn = h("input", { type: "file", accept: ".toml,.json,application/json", style: "display:none" });
|
||||
fileIn.addEventListener("change", async () => {
|
||||
const f = fileIn.files[0];
|
||||
if (!f) return;
|
||||
try {
|
||||
const cfg = JSON.parse(await f.text());
|
||||
await api.putConfig(cfg);
|
||||
const text = await f.text();
|
||||
if (f.name.endsWith(".json")) {
|
||||
await api.putConfig(JSON.parse(text)); // legacy JSON export
|
||||
} else {
|
||||
await postRaw("/config/import", text); // gateway.toml — validated + hot-reloaded
|
||||
}
|
||||
toast(t("toast.imported"), "good");
|
||||
} catch (e) {
|
||||
toast(String(e.message || e), "bad");
|
||||
@@ -503,6 +514,29 @@ export async function renderSettings() {
|
||||
fileIn.value = "";
|
||||
});
|
||||
|
||||
const restartBtn = h("button", { class: "btn danger" }, "⟳ " + t("settings.restart"));
|
||||
restartBtn.addEventListener("click", async () => {
|
||||
if (!(await confirmDialog(t("settings.restartConfirm")))) return;
|
||||
restartBtn.disabled = true;
|
||||
try {
|
||||
await api.restart();
|
||||
toast(t("settings.restarting"), "good");
|
||||
// the gateway comes back on the same port — reload once it answers
|
||||
const poll = setInterval(async () => {
|
||||
try {
|
||||
await api.health();
|
||||
clearInterval(poll);
|
||||
location.reload();
|
||||
} catch {
|
||||
/* still restarting */
|
||||
}
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
toast(String(e.message || e), "bad");
|
||||
restartBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
mount(
|
||||
root,
|
||||
h(
|
||||
@@ -623,7 +657,9 @@ export async function renderSettings() {
|
||||
{ class: "flex wrap" },
|
||||
h("button", { class: "btn", onClick: exportConfig }, t("settings.export")),
|
||||
h("button", { class: "btn", onClick: () => fileIn.click() }, t("settings.import")),
|
||||
fileIn
|
||||
fileIn,
|
||||
h("div", { class: "grow" }),
|
||||
restartBtn
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user