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
+64
-1
@@ -48,6 +48,68 @@ export async function renderKeys() {
|
||||
const input = h("input", { value: key, readonly: true, class: "mono" });
|
||||
const copyBtn = h("button", { class: "btn", onClick: () => { input.select(); navigator.clipboard?.writeText(key); toast(t("common.copied"), "good"); } }, t("common.copy"));
|
||||
modal(t("common.ok"), h("div", {}, h("p", {}, t("keys.created")), h("div", { class: "flex" }, input, copyBtn)), null);
|
||||
lastCreatedKey = key;
|
||||
reload();
|
||||
}
|
||||
|
||||
let lastCreatedKey = null;
|
||||
|
||||
// "Connect your app" — copy-paste snippets with this gateway's real URL.
|
||||
// Keys are stored hashed-masked in the list, so the key itself is a
|
||||
// placeholder unless one was just created on this page.
|
||||
function snippetsCard(openMode) {
|
||||
const base = location.origin + "/v1";
|
||||
const key = lastCreatedKey || (openMode ? "sk-anything" : "<" + t("keys.snip.keyPh") + ">");
|
||||
const snips = {
|
||||
curl: `curl ${base}/chat/completions \\
|
||||
-H "Authorization: Bearer ${key}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"model":"cortiq-auto","messages":[{"role":"user","content":"Привет!"}]}'`,
|
||||
python: `from openai import OpenAI
|
||||
|
||||
client = OpenAI(base_url="${base}", api_key="${key}")
|
||||
resp = client.chat.completions.create(
|
||||
model="cortiq-auto", # авто-роутинг; или точное имя модели
|
||||
messages=[{"role": "user", "content": "Привет!"}],
|
||||
)
|
||||
print(resp.choices[0].message.content)`,
|
||||
javascript: `import OpenAI from "openai";
|
||||
|
||||
const client = new OpenAI({ baseURL: "${base}", apiKey: "${key}" });
|
||||
const resp = await client.chat.completions.create({
|
||||
model: "cortiq-auto",
|
||||
messages: [{ role: "user", content: "Привет!" }],
|
||||
});
|
||||
console.log(resp.choices[0].message.content);`,
|
||||
};
|
||||
let cur = "curl";
|
||||
const pre = h("pre", { class: "conv-log snippet" }, snips[cur]);
|
||||
const seg = h(
|
||||
"div",
|
||||
{ class: "seg" },
|
||||
...Object.keys(snips).map((k, i) =>
|
||||
h("button", { class: i === 0 ? "active" : "", onClick: (e) => {
|
||||
cur = k;
|
||||
pre.textContent = snips[k];
|
||||
seg.querySelectorAll("button").forEach((b) => b.classList.remove("active"));
|
||||
e.target.classList.add("active");
|
||||
} }, k)
|
||||
)
|
||||
);
|
||||
const copy = h("button", { class: "btn ghost sm" }, "⧉ " + t("common.copy"));
|
||||
copy.addEventListener("click", async () => {
|
||||
await navigator.clipboard.writeText(pre.textContent);
|
||||
toast(t("common.copied"), "ok");
|
||||
});
|
||||
return h(
|
||||
"div",
|
||||
{ class: "card" },
|
||||
h("div", { class: "card-title" }, "🔌 " + t("keys.snip.title")),
|
||||
h("div", { class: "muted small", style: "margin-bottom:8px" },
|
||||
lastCreatedKey ? t("keys.snip.withKey") : openMode ? t("keys.snip.openMode") : t("keys.snip.hint")),
|
||||
h("div", { class: "flex", style: "gap:8px; margin-bottom:8px" }, seg, h("div", { class: "grow" }), copy),
|
||||
pre
|
||||
);
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
@@ -119,7 +181,8 @@ export async function renderKeys() {
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
snippetsCard(!!data.open_mode)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user