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:
infosave2007
2026-08-07 17:07:48 +03:00
parent a77beeabab
commit 15de36be3b
32 changed files with 1876 additions and 143 deletions
+5 -2
View File
@@ -45,6 +45,7 @@ export function renderOnboarding() {
const root = h("div", { class: "onb-wrap" });
let jobsPoll = null;
let jobs = [];
let sysSnapshot = null; // disk/RAM для плашек "не влезет" на карточках
let jobStates = new Map(); // job id → last seen state, to spot completions
let hosts = new Map(); // job id → card progress host (rebuilt on every render)
let jobsHost = h("div", {});
@@ -131,6 +132,7 @@ export function renderOnboarding() {
async function loadFeatured() {
featured = await fetchFeatured();
try { sysSnapshot = await api.system(); } catch { /* optional */ }
featuredLoaded = true;
}
@@ -199,7 +201,7 @@ export function renderOnboarding() {
);
} else if (step === 1) {
const top = topNav(prev, next, h("button", { class: "btn ghost", onClick: () => { step = 3; render(); } }, t("onboarding.skip")));
const ctx = { hosts, onDownload: startDownload, onDelete: handleDelete };
const ctx = { hosts, onDownload: startDownload, onDelete: handleDelete, sys: sysSnapshot };
// the list is fetched from HuggingFace — say so instead of showing a blank grid
const grid = featured.length
? h("div", { class: "hf-grid" }, ...featured.map((m) => featuredCard(m, ctx)))
@@ -290,7 +292,8 @@ export function renderOnboarding() {
// let the toast be readable before the reload swaps the page
await new Promise((res) => setTimeout(res, 900));
}
location.hash = "#/models";
// land where the reward is: try the model right away
location.hash = "#/playground";
location.reload();
});
const skipBtn = h("button", { class: "btn ghost", onClick: async () => {