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
+39
View File
@@ -506,4 +506,43 @@ export default {
"pg.noMediaModels": "Noch kein lokales {kind}-Modell. Lade eines (z. B. aus der infosave-Sammlung) unter Import herunter:",
"pg.tokSpeed": "Geschwindigkeit",
"pg.ttft": "Erstes Token",
// ops: logs, system, notices, gallery, chat, snippets, bench
"nav.logs": "Logs",
"logs.subtitle": "Jüngstes Gateway-Log — das, was das Terminal zeigen würde.",
"logs.filterPlaceholder": "filtern…",
"logs.pause": "Pause",
"logs.resume": "Fortsetzen",
"logs.empty": "Nichts gefunden.",
"notice.openAdmin": "Die Konsole ist ohne Token offen und von anderen Rechnern erreichbar. Setze einen Token, wenn diese Maschine nicht privat ist.",
"notice.update": "Update verfügbar: v{v}.",
"dash.sys.title": "System",
"dash.sys.cores": "Kerne",
"dash.sys.disk": "Festplatte (Modelle)",
"dash.sys.free": "frei",
"dash.sys.gateway": "Gateway",
"fit.disk": "zu wenig Speicherplatz",
"fit.diskTitle": "Der Download ist größer als der freie Platz auf der Modell-Festplatte.",
"fit.ram": "passt evtl. nicht in den RAM",
"fit.ramTitle": "Das Modell ist nahe an oder größer als der RAM dieser Maschine — es läuft womöglich nicht.",
"pg.eta": "übrig",
"pg.min": "Min",
"pg.queued": "In der Warteschlange — Position {n}. Startet, sobald die aktuelle Generierung fertig ist.",
"pg.gallery": "Letzte Generierungen",
"pg.galDeleteConfirm": "Diese Generierung samt Dateien löschen?",
"pg.clearChat": "Leeren",
"keys.snip.title": "Deine App verbinden",
"keys.snip.hint": "Füge eines der Beispiele in dein Projekt ein — nur der Schlüssel-Platzhalter muss ersetzt werden.",
"keys.snip.withKey": "Die Beispiele unten enthalten bereits den gerade erstellten Schlüssel.",
"keys.snip.openMode": "Keine API-Schlüssel konfiguriert — das Gateway akzeptiert jeden Wert.",
"keys.snip.keyPh": "dein API-Schlüssel",
"settings.restart": "Gateway neu starten",
"settings.restartConfirm": "Gateway neu starten? Laufende Anfragen brechen ab; lokale Modellserver starten ebenfalls neu.",
"settings.restarting": "Neustart — die Seite lädt neu, sobald das Gateway wieder da ist…",
"bench.title": "Geschwindigkeit lokaler Modelle",
"bench.hint": "Führt `cortiq bench` auf dieser Maschine aus — echte tok/s, ein Modell nach dem anderen.",
"bench.run": "Messen",
"bench.running": "misst…",
"bench.failed": "fehlgeschlagen",
"bench.none": "Noch keine lokalen .cmf-Chat-Modelle.",
};
+39
View File
@@ -499,4 +499,43 @@ export default {
"pg.noMediaModels": "No local {kind} model yet. Download one (e.g. from the infosave collection) in Import:",
"pg.tokSpeed": "Speed",
"pg.ttft": "First token",
// ops: logs, system, notices, gallery, chat, snippets, bench
"nav.logs": "Logs",
"logs.subtitle": "Recent gateway log — what the terminal would show.",
"logs.filterPlaceholder": "filter…",
"logs.pause": "Pause",
"logs.resume": "Resume",
"logs.empty": "Nothing matches.",
"notice.openAdmin": "The admin console is open without a token and reachable from other machines. Set a token if this box is not private.",
"notice.update": "Update available: v{v}.",
"dash.sys.title": "System",
"dash.sys.cores": "cores",
"dash.sys.disk": "Disk (models)",
"dash.sys.free": "free",
"dash.sys.gateway": "gateway",
"fit.disk": "not enough disk",
"fit.diskTitle": "The download is larger than the free space on the models disk.",
"fit.ram": "may not fit in RAM",
"fit.ramTitle": "The model is close to or larger than this machine's RAM — it may not run.",
"pg.eta": "left",
"pg.min": "min",
"pg.queued": "In queue — position {n}. It starts as soon as the current generation finishes.",
"pg.gallery": "Recent generations",
"pg.galDeleteConfirm": "Delete this generation and its files?",
"pg.clearChat": "Clear",
"keys.snip.title": "Connect your app",
"keys.snip.hint": "Paste one of these into your project — only the API key placeholder needs replacing.",
"keys.snip.withKey": "Snippets below include the key you just created.",
"keys.snip.openMode": "No API keys are configured — the gateway accepts any key value.",
"keys.snip.keyPh": "your API key",
"settings.restart": "Restart gateway",
"settings.restartConfirm": "Restart the gateway? Requests in flight will be dropped; local model servers restart too.",
"settings.restarting": "Restarting — the page reloads when the gateway is back…",
"bench.title": "Local model speed",
"bench.hint": "Runs `cortiq bench` on this machine — real tok/s, one model at a time.",
"bench.run": "Measure",
"bench.running": "measuring…",
"bench.failed": "failed",
"bench.none": "No local .cmf chat models yet.",
};
+39
View File
@@ -506,4 +506,43 @@ export default {
"pg.noMediaModels": "Aún no hay un modelo {kind} local. Descarga uno (p. ej. de la colección infosave) en Importar:",
"pg.tokSpeed": "Velocidad",
"pg.ttft": "Primer token",
// ops: logs, system, notices, gallery, chat, snippets, bench
"nav.logs": "Registros",
"logs.subtitle": "Registro reciente de la pasarela — lo que mostraría la terminal.",
"logs.filterPlaceholder": "filtrar…",
"logs.pause": "Pausa",
"logs.resume": "Reanudar",
"logs.empty": "Nada coincide.",
"notice.openAdmin": "La consola está abierta sin token y accesible desde otras máquinas. Configura un token si esta máquina no es privada.",
"notice.update": "Actualización disponible: v{v}.",
"dash.sys.title": "Sistema",
"dash.sys.cores": "núcleos",
"dash.sys.disk": "Disco (modelos)",
"dash.sys.free": "libre",
"dash.sys.gateway": "pasarela",
"fit.disk": "no hay espacio suficiente",
"fit.diskTitle": "La descarga supera el espacio libre del disco de modelos.",
"fit.ram": "puede no caber en RAM",
"fit.ramTitle": "El modelo se acerca o supera la RAM de esta máquina — puede que no funcione.",
"pg.eta": "restante",
"pg.min": "min",
"pg.queued": "En cola — posición {n}. Arranca en cuanto termine la generación actual.",
"pg.gallery": "Generaciones recientes",
"pg.galDeleteConfirm": "¿Eliminar esta generación y sus archivos?",
"pg.clearChat": "Limpiar",
"keys.snip.title": "Conecta tu aplicación",
"keys.snip.hint": "Pega uno de estos ejemplos en tu proyecto — solo hay que sustituir el marcador de la clave.",
"keys.snip.withKey": "Los ejemplos de abajo ya incluyen la clave que acabas de crear.",
"keys.snip.openMode": "No hay claves API configuradas — la pasarela acepta cualquier valor.",
"keys.snip.keyPh": "tu clave API",
"settings.restart": "Reiniciar la pasarela",
"settings.restartConfirm": "¿Reiniciar la pasarela? Las peticiones en curso se cortarán; los servidores de modelos locales también se reinician.",
"settings.restarting": "Reiniciando — la página se recargará cuando la pasarela vuelva…",
"bench.title": "Velocidad de modelos locales",
"bench.hint": "Ejecuta `cortiq bench` en esta máquina — tok/s reales, un modelo a la vez.",
"bench.run": "Medir",
"bench.running": "midiendo…",
"bench.failed": "falló",
"bench.none": "Aún no hay modelos de chat .cmf locales.",
};
+39
View File
@@ -506,4 +506,43 @@ export default {
"pg.noMediaModels": "Pas encore de modèle {kind} local. Téléchargez-en un (par ex. de la collection infosave) dans Importer :",
"pg.tokSpeed": "Vitesse",
"pg.ttft": "Premier token",
// ops: logs, system, notices, gallery, chat, snippets, bench
"nav.logs": "Journaux",
"logs.subtitle": "Journal récent de la passerelle — ce que montrerait le terminal.",
"logs.filterPlaceholder": "filtrer…",
"logs.pause": "Pause",
"logs.resume": "Reprendre",
"logs.empty": "Rien ne correspond.",
"notice.openAdmin": "La console est ouverte sans jeton et accessible depuis d'autres machines. Définissez un jeton si cette machine n'est pas privée.",
"notice.update": "Mise à jour disponible : v{v}.",
"dash.sys.title": "Système",
"dash.sys.cores": "cœurs",
"dash.sys.disk": "Disque (modèles)",
"dash.sys.free": "libre",
"dash.sys.gateway": "passerelle",
"fit.disk": "espace disque insuffisant",
"fit.diskTitle": "Le téléchargement dépasse l'espace libre du disque des modèles.",
"fit.ram": "risque de ne pas tenir en RAM",
"fit.ramTitle": "Le modèle approche ou dépasse la RAM de cette machine — il pourrait ne pas tourner.",
"pg.eta": "restant",
"pg.min": "min",
"pg.queued": "En file d'attente — position {n}. Démarre dès la fin de la génération en cours.",
"pg.gallery": "Générations récentes",
"pg.galDeleteConfirm": "Supprimer cette génération et ses fichiers ?",
"pg.clearChat": "Effacer",
"keys.snip.title": "Connecter votre application",
"keys.snip.hint": "Collez un de ces exemples dans votre projet — seul le espace réservé de la clé est à remplacer.",
"keys.snip.withKey": "Les exemples ci-dessous incluent la clé que vous venez de créer.",
"keys.snip.openMode": "Aucune clé API configurée — la passerelle accepte n'importe quelle valeur.",
"keys.snip.keyPh": "votre clé API",
"settings.restart": "Redémarrer la passerelle",
"settings.restartConfirm": "Redémarrer la passerelle ? Les requêtes en cours seront coupées ; les serveurs de modèles locaux redémarrent aussi.",
"settings.restarting": "Redémarrage — la page se recharge dès que la passerelle revient…",
"bench.title": "Vitesse des modèles locaux",
"bench.hint": "Exécute `cortiq bench` sur cette machine — de vrais tok/s, un modèle à la fois.",
"bench.run": "Mesurer",
"bench.running": "mesure…",
"bench.failed": "échec",
"bench.none": "Pas encore de modèles de chat .cmf locaux.",
};
+39
View File
@@ -498,4 +498,43 @@ export default {
"pg.noMediaModels": "Локальной модели ({kind}) ещё нет. Скачайте её (например, из коллекции infosave) во вкладке Импорт:",
"pg.tokSpeed": "Скорость",
"pg.ttft": "Первый токен",
// ops: logs, system, notices, gallery, chat, snippets, bench
"nav.logs": "Логи",
"logs.subtitle": "Последние записи журнала шлюза — то, что показал бы терминал.",
"logs.filterPlaceholder": "фильтр…",
"logs.pause": "Пауза",
"logs.resume": "Продолжить",
"logs.empty": "Ничего не найдено.",
"notice.openAdmin": "Панель управления открыта без токена и доступна с других машин. Задайте токен, если эта машина не приватная.",
"notice.update": "Доступно обновление: v{v}.",
"dash.sys.title": "Система",
"dash.sys.cores": "ядер",
"dash.sys.disk": "Диск (модели)",
"dash.sys.free": "свободно",
"dash.sys.gateway": "шлюз",
"fit.disk": "не хватит места",
"fit.diskTitle": "Загрузка больше свободного места на диске с моделями.",
"fit.ram": "может не влезть в RAM",
"fit.ramTitle": "Модель сравнима с объёмом RAM этой машины или больше — может не запуститься.",
"pg.eta": "осталось",
"pg.min": "мин",
"pg.queued": "В очереди — позиция {n}. Стартует, как только закончится текущая генерация.",
"pg.gallery": "Последние генерации",
"pg.galDeleteConfirm": "Удалить эту генерацию и её файлы?",
"pg.clearChat": "Очистить",
"keys.snip.title": "Подключите ваше приложение",
"keys.snip.hint": "Вставьте один из примеров в свой проект — заменить нужно только плейсхолдер ключа.",
"keys.snip.withKey": "В примерах ниже уже подставлен только что созданный ключ.",
"keys.snip.openMode": "API-ключи не настроены — шлюз принимает любое значение ключа.",
"keys.snip.keyPh": "ваш API-ключ",
"settings.restart": "Перезапустить шлюз",
"settings.restartConfirm": "Перезапустить шлюз? Текущие запросы оборвутся; серверы локальных моделей тоже перезапустятся.",
"settings.restarting": "Перезапуск — страница обновится, когда шлюз поднимется…",
"bench.title": "Скорость локальных моделей",
"bench.hint": "Запускает `cortiq bench` на этой машине — реальные tok/s, по одной модели за раз.",
"bench.run": "Замерить",
"bench.running": "замер…",
"bench.failed": "ошибка",
"bench.none": "Локальных чат-моделей .cmf пока нет.",
};
+39
View File
@@ -506,4 +506,43 @@ export default {
"pg.noMediaModels": "Henüz yerel bir {kind} modeli yok. İçe aktarma sekmesinden bir tane indirin (ör. infosave koleksiyonundan):",
"pg.tokSpeed": "Hız",
"pg.ttft": "İlk token",
// ops: logs, system, notices, gallery, chat, snippets, bench
"nav.logs": "Günlükler",
"logs.subtitle": "Ağ geçidinin son günlüğü — terminalde görecekleriniz.",
"logs.filterPlaceholder": "filtrele…",
"logs.pause": "Duraklat",
"logs.resume": "Devam et",
"logs.empty": "Eşleşen yok.",
"notice.openAdmin": "Yönetim konsolu belirteçsiz açık ve diğer makinelerden erişilebilir. Bu makine özel değilse bir belirteç ayarlayın.",
"notice.update": "Güncelleme mevcut: v{v}.",
"dash.sys.title": "Sistem",
"dash.sys.cores": "çekirdek",
"dash.sys.disk": "Disk (modeller)",
"dash.sys.free": "boş",
"dash.sys.gateway": "ağ geçidi",
"fit.disk": "disk alanı yetmez",
"fit.diskTitle": "İndirme, model diskindeki boş alandan büyük.",
"fit.ram": "RAM'e sığmayabilir",
"fit.ramTitle": "Model bu makinenin RAM'ine yakın ya da ondan büyük — çalışmayabilir.",
"pg.eta": "kaldı",
"pg.min": "dk",
"pg.queued": "Kuyrukta — {n}. sırada. Mevcut üretim biter bitmez başlar.",
"pg.gallery": "Son üretimler",
"pg.galDeleteConfirm": "Bu üretim ve dosyaları silinsin mi?",
"pg.clearChat": "Temizle",
"keys.snip.title": "Uygulamanı bağla",
"keys.snip.hint": "Örneklerden birini projene yapıştır — yalnızca anahtar yer tutucusunu değiştirmen gerekir.",
"keys.snip.withKey": "Aşağıdaki örnekler az önce oluşturduğun anahtarı içeriyor.",
"keys.snip.openMode": "API anahtarı yapılandırılmadı — ağ geçidi her değeri kabul eder.",
"keys.snip.keyPh": "API anahtarın",
"settings.restart": "Ağ geçidini yeniden başlat",
"settings.restartConfirm": "Ağ geçidi yeniden başlatılsın mı? Devam eden istekler kesilir; yerel model sunucuları da yeniden başlar.",
"settings.restarting": "Yeniden başlatılıyor — ağ geçidi dönünce sayfa yenilenecek…",
"bench.title": "Yerel model hızı",
"bench.hint": "Bu makinede `cortiq bench` çalıştırır — gerçek tok/s, aynı anda tek model.",
"bench.run": "Ölç",
"bench.running": "ölçülüyor…",
"bench.failed": "başarısız",
"bench.none": "Henüz yerel .cmf sohbet modeli yok.",
};
+39
View File
@@ -506,4 +506,43 @@ export default {
"pg.noMediaModels": "还没有本地{kind}模型。请在“导入模型”中下载一个(例如 infosave 合集):",
"pg.tokSpeed": "速度",
"pg.ttft": "首个 token",
// ops: logs, system, notices, gallery, chat, snippets, bench
"nav.logs": "日志",
"logs.subtitle": "网关最近的日志 — 终端里会看到的内容。",
"logs.filterPlaceholder": "过滤……",
"logs.pause": "暂停",
"logs.resume": "继续",
"logs.empty": "没有匹配内容。",
"notice.openAdmin": "管理控制台未设置令牌即可从其他机器访问。若这台机器不是私有的,请设置令牌。",
"notice.update": "有可用更新:v{v}。",
"dash.sys.title": "系统",
"dash.sys.cores": "核",
"dash.sys.disk": "磁盘(模型)",
"dash.sys.free": "可用",
"dash.sys.gateway": "网关",
"fit.disk": "磁盘空间不足",
"fit.diskTitle": "下载大小超过模型磁盘的可用空间。",
"fit.ram": "可能超出内存",
"fit.ramTitle": "模型接近或超过本机内存 — 可能无法运行。",
"pg.eta": "剩余",
"pg.min": "分钟",
"pg.queued": "排队中 — 第 {n} 位。当前生成结束后立即开始。",
"pg.gallery": "最近的生成",
"pg.galDeleteConfirm": "删除这次生成及其文件?",
"pg.clearChat": "清空",
"keys.snip.title": "接入你的应用",
"keys.snip.hint": "把示例粘贴到你的项目里 — 只需替换密钥占位符。",
"keys.snip.withKey": "下面的示例已包含你刚创建的密钥。",
"keys.snip.openMode": "未配置 API 密钥 — 网关接受任意密钥值。",
"keys.snip.keyPh": "你的 API 密钥",
"settings.restart": "重启网关",
"settings.restartConfirm": "重启网关?进行中的请求会中断;本地模型服务器也会重启。",
"settings.restarting": "正在重启 — 网关恢复后页面会自动刷新……",
"bench.title": "本地模型速度",
"bench.hint": "在本机运行 `cortiq bench` — 真实 tok/s,一次一个模型。",
"bench.run": "测速",
"bench.running": "测速中……",
"bench.failed": "失败",
"bench.none": "还没有本地 .cmf 聊天模型。",
};