feat(admin): power controls — stop everything & power off, restart everything
Settings gains a second service button next to restart: stop every managed local model server and exit the gateway process (nothing is respawned; in Docker the restart policy decides). The SPA replaces itself with a static "gateway is off" screen since the API is gone. The restart button is relabeled "Restart everything" to say what it always did — the gateway re-execs itself and managed servers come back with it. Translated in all seven languages.
This commit is contained in:
@@ -184,6 +184,7 @@ pub fn api_routes(admin_token: String) -> Router<SharedState> {
|
|||||||
.route("/admin/api/logs", get(get_logs).delete(clear_logs))
|
.route("/admin/api/logs", get(get_logs).delete(clear_logs))
|
||||||
.route("/admin/api/system", get(get_system))
|
.route("/admin/api/system", get(get_system))
|
||||||
.route("/admin/api/restart", post(restart_gateway))
|
.route("/admin/api/restart", post(restart_gateway))
|
||||||
|
.route("/admin/api/shutdown", post(shutdown_gateway))
|
||||||
.route("/admin/api/bench", get(bench_list).post(bench_start))
|
.route("/admin/api/bench", get(bench_list).post(bench_start))
|
||||||
.route("/admin/api/config/export", get(config_export))
|
.route("/admin/api/config/export", get(config_export))
|
||||||
.route("/admin/api/config/import", post(config_import))
|
.route("/admin/api/config/import", post(config_import))
|
||||||
@@ -2272,6 +2273,20 @@ async fn get_system(State(state): State<SharedState>) -> ApiResult {
|
|||||||
ok(serde_json::to_value(snap).unwrap_or_else(|_| json!({})))
|
ok(serde_json::to_value(snap).unwrap_or_else(|_| json!({})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stop every managed local model server and power the gateway off. The 200
|
||||||
|
/// goes out first so the SPA can show its "gateway is off" screen; nothing is
|
||||||
|
/// respawned — the operator starts the gateway again by hand (or the Docker
|
||||||
|
/// restart policy does, in which case this behaves like a restart).
|
||||||
|
async fn shutdown_gateway(State(state): State<SharedState>) -> ApiResult {
|
||||||
|
state.cmf.stop().await;
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
|
||||||
|
tracing::info!("shutdown requested from the admin console");
|
||||||
|
std::process::exit(0);
|
||||||
|
});
|
||||||
|
ok(json!({ "ok": true }))
|
||||||
|
}
|
||||||
|
|
||||||
/// Restart the gateway in place: stop managed model servers, spawn a fresh
|
/// Restart the gateway in place: stop managed model servers, spawn a fresh
|
||||||
/// copy of ourselves (it retries the bind while our port frees), then exit.
|
/// copy of ourselves (it retries the bind while our port frees), then exit.
|
||||||
/// In Docker we only exit — we are PID 1, a spawned child would die with the
|
/// In Docker we only exit — we are PID 1, a spawned child would die with the
|
||||||
|
|||||||
Vendored
+5
@@ -706,3 +706,8 @@ tbody tr:last-child { animation: bm-in .25s ease; }
|
|||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.sidebar, .mobile-nav-backdrop { transition: none; }
|
.sidebar, .mobile-nav-backdrop { transition: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* full-screen farewell after "stop everything & power off" */
|
||||||
|
.shutdown-screen { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; gap: 12px; text-align: center; padding: 24px; }
|
||||||
|
.shutdown-screen .shutdown-icon { font-size: 64px; color: var(--bad); }
|
||||||
|
.shutdown-screen .hint { max-width: 480px; }
|
||||||
|
|||||||
Vendored
+1
@@ -134,6 +134,7 @@ export const api = {
|
|||||||
logs: (limit = 300) => j("GET", "/logs?limit=" + limit),
|
logs: (limit = 300) => j("GET", "/logs?limit=" + limit),
|
||||||
clearLogs: () => j("DELETE", "/logs"),
|
clearLogs: () => j("DELETE", "/logs"),
|
||||||
restart: () => j("POST", "/restart"),
|
restart: () => j("POST", "/restart"),
|
||||||
|
shutdown: () => j("POST", "/shutdown"),
|
||||||
benchStart: (model) => j("POST", "/bench", { model }),
|
benchStart: (model) => j("POST", "/bench", { model }),
|
||||||
benchList: () => j("GET", "/bench"),
|
benchList: () => j("GET", "/bench"),
|
||||||
|
|
||||||
|
|||||||
Vendored
+6
-2
@@ -536,9 +536,13 @@ export default {
|
|||||||
"keys.snip.withKey": "Die Beispiele unten enthalten bereits den gerade erstellten Schlüssel.",
|
"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.openMode": "Keine API-Schlüssel konfiguriert — das Gateway akzeptiert jeden Wert.",
|
||||||
"keys.snip.keyPh": "dein API-Schlüssel",
|
"keys.snip.keyPh": "dein API-Schlüssel",
|
||||||
"settings.restart": "Gateway neu starten",
|
"settings.restart": "Alles neu starten",
|
||||||
"settings.restartConfirm": "Gateway neu starten? Laufende Anfragen brechen ab; lokale Modellserver starten ebenfalls neu.",
|
"settings.restartConfirm": "Gateway und alle lokalen Modellserver neu starten? Laufende Anfragen werden abgebrochen.",
|
||||||
"settings.restarting": "Neustart — die Seite lädt neu, sobald das Gateway wieder da ist…",
|
"settings.restarting": "Neustart — die Seite lädt neu, sobald das Gateway wieder da ist…",
|
||||||
|
"settings.shutdown": "Alles stoppen & ausschalten",
|
||||||
|
"settings.shutdownConfirm": "Alle lokalen Modellserver stoppen und das Gateway ausschalten? API und dieses Panel antworten erst wieder, wenn Sie das Gateway neu starten.",
|
||||||
|
"settings.shutdownDone": "Das Gateway ist ausgeschaltet.",
|
||||||
|
"settings.shutdownHint": "Alle lokalen Modellserver sind gestoppt, der Prozess wurde beendet. Starten Sie das Gateway auf dem Server neu, um das Panel zurückzubekommen.",
|
||||||
"bench.title": "Geschwindigkeit lokaler Modelle",
|
"bench.title": "Geschwindigkeit lokaler Modelle",
|
||||||
"bench.hint": "Führt `cortiq bench` auf dieser Maschine aus — echte tok/s, ein Modell nach dem anderen.",
|
"bench.hint": "Führt `cortiq bench` auf dieser Maschine aus — echte tok/s, ein Modell nach dem anderen.",
|
||||||
"bench.run": "Messen",
|
"bench.run": "Messen",
|
||||||
|
|||||||
Vendored
+6
-2
@@ -529,9 +529,13 @@ export default {
|
|||||||
"keys.snip.withKey": "Snippets below include the key you just created.",
|
"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.openMode": "No API keys are configured — the gateway accepts any key value.",
|
||||||
"keys.snip.keyPh": "your API key",
|
"keys.snip.keyPh": "your API key",
|
||||||
"settings.restart": "Restart gateway",
|
"settings.restart": "Restart everything",
|
||||||
"settings.restartConfirm": "Restart the gateway? Requests in flight will be dropped; local model servers restart too.",
|
"settings.restartConfirm": "Restart the gateway and all local model servers? Requests in flight will be dropped.",
|
||||||
"settings.restarting": "Restarting — the page reloads when the gateway is back…",
|
"settings.restarting": "Restarting — the page reloads when the gateway is back…",
|
||||||
|
"settings.shutdown": "Stop everything & power off",
|
||||||
|
"settings.shutdownConfirm": "Stop all local model servers and power the gateway off? The API and this panel stop answering until you start the gateway again.",
|
||||||
|
"settings.shutdownDone": "The gateway is off.",
|
||||||
|
"settings.shutdownHint": "All local model servers are stopped and the process has exited. Start the gateway again on the server to bring the panel back.",
|
||||||
"bench.title": "Local model speed",
|
"bench.title": "Local model speed",
|
||||||
"bench.hint": "Runs `cortiq bench` on this machine — real tok/s, one model at a time.",
|
"bench.hint": "Runs `cortiq bench` on this machine — real tok/s, one model at a time.",
|
||||||
"bench.run": "Measure",
|
"bench.run": "Measure",
|
||||||
|
|||||||
Vendored
+6
-2
@@ -536,9 +536,13 @@ export default {
|
|||||||
"keys.snip.withKey": "Los ejemplos de abajo ya incluyen la clave que acabas de crear.",
|
"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.openMode": "No hay claves API configuradas — la pasarela acepta cualquier valor.",
|
||||||
"keys.snip.keyPh": "tu clave API",
|
"keys.snip.keyPh": "tu clave API",
|
||||||
"settings.restart": "Reiniciar la pasarela",
|
"settings.restart": "Reiniciar todo",
|
||||||
"settings.restartConfirm": "¿Reiniciar la pasarela? Las peticiones en curso se cortarán; los servidores de modelos locales también se reinician.",
|
"settings.restartConfirm": "¿Reiniciar la pasarela y todos los servidores de modelos locales? Las solicitudes en curso se interrumpirán.",
|
||||||
"settings.restarting": "Reiniciando — la página se recargará cuando la pasarela vuelva…",
|
"settings.restarting": "Reiniciando — la página se recargará cuando la pasarela vuelva…",
|
||||||
|
"settings.shutdown": "Detener todo y apagar",
|
||||||
|
"settings.shutdownConfirm": "¿Detener todos los servidores de modelos locales y apagar la pasarela? La API y este panel dejarán de responder hasta que vuelva a iniciar la pasarela.",
|
||||||
|
"settings.shutdownDone": "La pasarela está apagada.",
|
||||||
|
"settings.shutdownHint": "Todos los servidores de modelos locales están detenidos y el proceso ha terminado. Inicie de nuevo la pasarela en el servidor para recuperar el panel.",
|
||||||
"bench.title": "Velocidad de modelos locales",
|
"bench.title": "Velocidad de modelos locales",
|
||||||
"bench.hint": "Ejecuta `cortiq bench` en esta máquina — tok/s reales, un modelo a la vez.",
|
"bench.hint": "Ejecuta `cortiq bench` en esta máquina — tok/s reales, un modelo a la vez.",
|
||||||
"bench.run": "Medir",
|
"bench.run": "Medir",
|
||||||
|
|||||||
Vendored
+6
-2
@@ -536,9 +536,13 @@ export default {
|
|||||||
"keys.snip.withKey": "Les exemples ci-dessous incluent la clé que vous venez de créer.",
|
"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.openMode": "Aucune clé API configurée — la passerelle accepte n'importe quelle valeur.",
|
||||||
"keys.snip.keyPh": "votre clé API",
|
"keys.snip.keyPh": "votre clé API",
|
||||||
"settings.restart": "Redémarrer la passerelle",
|
"settings.restart": "Tout redémarrer",
|
||||||
"settings.restartConfirm": "Redémarrer la passerelle ? Les requêtes en cours seront coupées ; les serveurs de modèles locaux redémarrent aussi.",
|
"settings.restartConfirm": "Redémarrer la passerelle et tous les serveurs de modèles locaux ? Les requêtes en cours seront interrompues.",
|
||||||
"settings.restarting": "Redémarrage — la page se recharge dès que la passerelle revient…",
|
"settings.restarting": "Redémarrage — la page se recharge dès que la passerelle revient…",
|
||||||
|
"settings.shutdown": "Tout arrêter et éteindre",
|
||||||
|
"settings.shutdownConfirm": "Arrêter tous les serveurs de modèles locaux et éteindre la passerelle ? L'API et ce panneau ne répondront plus tant que vous n'aurez pas relancé la passerelle.",
|
||||||
|
"settings.shutdownDone": "La passerelle est éteinte.",
|
||||||
|
"settings.shutdownHint": "Tous les serveurs de modèles locaux sont arrêtés et le processus est terminé. Relancez la passerelle sur le serveur pour retrouver le panneau.",
|
||||||
"bench.title": "Vitesse des modèles locaux",
|
"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.hint": "Exécute `cortiq bench` sur cette machine — de vrais tok/s, un modèle à la fois.",
|
||||||
"bench.run": "Mesurer",
|
"bench.run": "Mesurer",
|
||||||
|
|||||||
Vendored
+6
-2
@@ -528,9 +528,13 @@ export default {
|
|||||||
"keys.snip.withKey": "В примерах ниже уже подставлен только что созданный ключ.",
|
"keys.snip.withKey": "В примерах ниже уже подставлен только что созданный ключ.",
|
||||||
"keys.snip.openMode": "API-ключи не настроены — шлюз принимает любое значение ключа.",
|
"keys.snip.openMode": "API-ключи не настроены — шлюз принимает любое значение ключа.",
|
||||||
"keys.snip.keyPh": "ваш API-ключ",
|
"keys.snip.keyPh": "ваш API-ключ",
|
||||||
"settings.restart": "Перезапустить шлюз",
|
"settings.restart": "Перезапустить всё",
|
||||||
"settings.restartConfirm": "Перезапустить шлюз? Текущие запросы оборвутся; серверы локальных моделей тоже перезапустятся.",
|
"settings.restartConfirm": "Перезапустить шлюз и все серверы локальных моделей? Текущие запросы оборвутся.",
|
||||||
"settings.restarting": "Перезапуск — страница обновится, когда шлюз поднимется…",
|
"settings.restarting": "Перезапуск — страница обновится, когда шлюз поднимется…",
|
||||||
|
"settings.shutdown": "Остановить всё и выключить",
|
||||||
|
"settings.shutdownConfirm": "Остановить все серверы локальных моделей и выключить шлюз? API и эта панель перестанут отвечать, пока вы не запустите шлюз заново.",
|
||||||
|
"settings.shutdownDone": "Шлюз выключен.",
|
||||||
|
"settings.shutdownHint": "Все серверы локальных моделей остановлены, процесс завершён. Запустите шлюз на сервере заново, чтобы вернуть панель.",
|
||||||
"bench.title": "Скорость локальных моделей",
|
"bench.title": "Скорость локальных моделей",
|
||||||
"bench.hint": "Запускает `cortiq bench` на этой машине — реальные tok/s, по одной модели за раз.",
|
"bench.hint": "Запускает `cortiq bench` на этой машине — реальные tok/s, по одной модели за раз.",
|
||||||
"bench.run": "Замерить",
|
"bench.run": "Замерить",
|
||||||
|
|||||||
Vendored
+6
-2
@@ -536,9 +536,13 @@ export default {
|
|||||||
"keys.snip.withKey": "Aşağıdaki örnekler az önce oluşturduğun anahtarı içeriyor.",
|
"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.openMode": "API anahtarı yapılandırılmadı — ağ geçidi her değeri kabul eder.",
|
||||||
"keys.snip.keyPh": "API anahtarın",
|
"keys.snip.keyPh": "API anahtarın",
|
||||||
"settings.restart": "Ağ geçidini yeniden başlat",
|
"settings.restart": "Tümünü 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.restartConfirm": "Ağ geçidi ve tüm yerel model sunucuları yeniden başlatılsın mı? Devam eden istekler kesilecek.",
|
||||||
"settings.restarting": "Yeniden başlatılıyor — ağ geçidi dönünce sayfa yenilenecek…",
|
"settings.restarting": "Yeniden başlatılıyor — ağ geçidi dönünce sayfa yenilenecek…",
|
||||||
|
"settings.shutdown": "Tümünü durdur ve kapat",
|
||||||
|
"settings.shutdownConfirm": "Tüm yerel model sunucuları durdurulup ağ geçidi kapatılsın mı? Ağ geçidini yeniden başlatana kadar API ve bu panel yanıt vermez.",
|
||||||
|
"settings.shutdownDone": "Ağ geçidi kapalı.",
|
||||||
|
"settings.shutdownHint": "Tüm yerel model sunucuları durduruldu ve süreç sonlandı. Paneli geri getirmek için ağ geçidini sunucuda yeniden başlatın.",
|
||||||
"bench.title": "Yerel model hızı",
|
"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.hint": "Bu makinede `cortiq bench` çalıştırır — gerçek tok/s, aynı anda tek model.",
|
||||||
"bench.run": "Ölç",
|
"bench.run": "Ölç",
|
||||||
|
|||||||
Vendored
+6
-2
@@ -536,9 +536,13 @@ export default {
|
|||||||
"keys.snip.withKey": "下面的示例已包含你刚创建的密钥。",
|
"keys.snip.withKey": "下面的示例已包含你刚创建的密钥。",
|
||||||
"keys.snip.openMode": "未配置 API 密钥 — 网关接受任意密钥值。",
|
"keys.snip.openMode": "未配置 API 密钥 — 网关接受任意密钥值。",
|
||||||
"keys.snip.keyPh": "你的 API 密钥",
|
"keys.snip.keyPh": "你的 API 密钥",
|
||||||
"settings.restart": "重启网关",
|
"settings.restart": "全部重启",
|
||||||
"settings.restartConfirm": "重启网关?进行中的请求会中断;本地模型服务器也会重启。",
|
"settings.restartConfirm": "重启网关和所有本地模型服务器?正在处理的请求将被中断。",
|
||||||
"settings.restarting": "正在重启 — 网关恢复后页面会自动刷新……",
|
"settings.restarting": "正在重启 — 网关恢复后页面会自动刷新……",
|
||||||
|
"settings.shutdown": "全部停止并关机",
|
||||||
|
"settings.shutdownConfirm": "停止所有本地模型服务器并关闭网关?在你重新启动网关之前,API 和此面板都将无法响应。",
|
||||||
|
"settings.shutdownDone": "网关已关闭。",
|
||||||
|
"settings.shutdownHint": "所有本地模型服务器已停止,进程已退出。请在服务器上重新启动网关以恢复面板。",
|
||||||
"bench.title": "本地模型速度",
|
"bench.title": "本地模型速度",
|
||||||
"bench.hint": "在本机运行 `cortiq bench` — 真实 tok/s,一次一个模型。",
|
"bench.hint": "在本机运行 `cortiq bench` — 真实 tok/s,一次一个模型。",
|
||||||
"bench.run": "测速",
|
"bench.run": "测速",
|
||||||
|
|||||||
Vendored
+27
-1
@@ -559,6 +559,31 @@ export async function renderSettings() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const stopBtn = h("button", { class: "btn danger" }, "\u23fb " + t("settings.shutdown"));
|
||||||
|
stopBtn.addEventListener("click", async () => {
|
||||||
|
if (!(await confirmDialog(t("settings.shutdownConfirm")))) return;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
restartBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
await api.shutdown();
|
||||||
|
// the server is gone in ~300ms — replace the app with a static farewell
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
document.body.appendChild(
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{ class: "shutdown-screen" },
|
||||||
|
h("div", { class: "shutdown-icon" }, "\u23fb"),
|
||||||
|
h("h2", {}, t("settings.shutdownDone")),
|
||||||
|
h("p", { class: "hint" }, t("settings.shutdownHint"))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
toast(String(e.message || e), "bad");
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
restartBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
mount(
|
mount(
|
||||||
root,
|
root,
|
||||||
h(
|
h(
|
||||||
@@ -682,7 +707,8 @@ export async function renderSettings() {
|
|||||||
h("button", { class: "btn", onClick: () => fileIn.click() }, t("settings.import")),
|
h("button", { class: "btn", onClick: () => fileIn.click() }, t("settings.import")),
|
||||||
fileIn,
|
fileIn,
|
||||||
h("div", { class: "grow" }),
|
h("div", { class: "grow" }),
|
||||||
restartBtn
|
restartBtn,
|
||||||
|
stopBtn
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user