From dd9ccaea94af5067d3af4b21f07708ef7ba5ce12 Mon Sep 17 00:00:00 2001 From: infosave2007 Date: Fri, 7 Aug 2026 22:14:08 +0300 Subject: [PATCH] fix(shell): stale tabs now know the gateway was updated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recurring confusion all day: the SPA lives in an open tab until reload, so after a gateway rebuild/update users kept seeing (and benchmarking with!) an old interface — "where is the feature", "why are the test ids old". The meta endpoint now carries the process start timestamp; the existing 20s health poll compares it, and when the gateway has restarted the tab shows a fixed banner: "The gateway was updated — this page is running an older interface" with a one-click Reload. 2 strings in all 7 languages. --- src/admin/mod.rs | 10 ++++++++++ web/js/app.js | 18 ++++++++++++++++++ web/js/i18n/de.js | 2 ++ web/js/i18n/en.js | 2 ++ web/js/i18n/es.js | 2 ++ web/js/i18n/fr.js | 2 ++ web/js/i18n/ru.js | 2 ++ web/js/i18n/tr.js | 2 ++ web/js/i18n/zh.js | 2 ++ 9 files changed, 42 insertions(+) diff --git a/src/admin/mod.rs b/src/admin/mod.rs index d89994c..3478586 100644 --- a/src/admin/mod.rs +++ b/src/admin/mod.rs @@ -600,8 +600,18 @@ async fn provider_models( } async fn meta(State(state): State) -> ApiResult { + // process start marker: open tabs poll this and offer a reload when the + // gateway (and thus the embedded SPA) has been restarted/updated + static STARTED: std::sync::OnceLock = std::sync::OnceLock::new(); + let started = *STARTED.get_or_init(|| { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + }); ok(json!({ "version": env!("CARGO_PKG_VERSION"), + "started": started, "update_available": state.gateway_update.lock().unwrap().clone(), "admin_open_public": state .admin_open_public diff --git a/web/js/app.js b/web/js/app.js index cb4e088..01bc7fd 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -256,12 +256,30 @@ function showLogin() { mount(document.getElementById("app"), renderLogin(boot)); } +let staleNotified = false; +async function checkUiFresh() { + // the SPA lives in the tab until reload; if the gateway restarted (dev + // rebuild, update, config restart) the embedded UI may have changed — + // tell the user instead of letting them stare at stale views + try { + const m = await api.meta(); + if (appState.meta?.started && m.started && m.started !== appState.meta.started && !staleNotified) { + staleNotified = true; + const bar = h("div", { class: "callout warn", style: "position:fixed; left:50%; transform:translateX(-50%); bottom:18px; z-index:99; box-shadow:0 8px 24px rgba(0,0,0,.35)" }, + h("span", {}, "🔄 " + t("shell.uiStale")), " ", + h("button", { class: "btn sm primary", onClick: () => location.reload() }, t("shell.reload"))); + document.body.append(bar); + } + } catch {} +} + async function refreshHealth() { try { appState.health = await api.health(); } catch { appState.health = null; } + checkUiFresh(); // re-render topbar only if shell present const route = currentRoute(); const main = document.querySelector(".main"); diff --git a/web/js/i18n/de.js b/web/js/i18n/de.js index 32edf8c..ade3bec 100644 --- a/web/js/i18n/de.js +++ b/web/js/i18n/de.js @@ -619,4 +619,6 @@ export default { "bm.degraded": "Router degradiert", "bm.routerDegraded": "Der Router hat nicht rechtzeitig geantwortet — die Anfrage nutzte die Fallback-Kette; die Klassifikation wird nicht gewertet.", "bm.cutByCap": "Budget ging fürs Denken drauf — keine Antwort mehr", + "shell.uiStale": "Das Gateway wurde aktualisiert — diese Seite zeigt eine ältere Oberfläche.", + "shell.reload": "Neu laden", }; diff --git a/web/js/i18n/en.js b/web/js/i18n/en.js index 64476e6..5efcfc8 100644 --- a/web/js/i18n/en.js +++ b/web/js/i18n/en.js @@ -612,4 +612,6 @@ export default { "bm.degraded": "router degraded", "bm.routerDegraded": "The router did not answer in time — the request used the fallback chain; classification is not judged.", "bm.cutByCap": "budget spent on reasoning — answer never came", + "shell.uiStale": "The gateway was updated — this page is running an older interface.", + "shell.reload": "Reload", }; diff --git a/web/js/i18n/es.js b/web/js/i18n/es.js index df380d5..bc00ecb 100644 --- a/web/js/i18n/es.js +++ b/web/js/i18n/es.js @@ -619,4 +619,6 @@ export default { "bm.degraded": "router degradado", "bm.routerDegraded": "El router no respondió a tiempo — la petición usó la cadena de reserva; la clasificación no se cuenta.", "bm.cutByCap": "presupuesto gastado en razonar — sin respuesta final", + "shell.uiStale": "La pasarela se actualizó — esta página usa una interfaz antigua.", + "shell.reload": "Recargar", }; diff --git a/web/js/i18n/fr.js b/web/js/i18n/fr.js index 2b09e49..78c3fa5 100644 --- a/web/js/i18n/fr.js +++ b/web/js/i18n/fr.js @@ -619,4 +619,6 @@ export default { "bm.degraded": "routeur dégradé", "bm.routerDegraded": "Le routeur n'a pas répondu à temps — la requête a suivi la chaîne de secours ; la classification n'est pas comptée.", "bm.cutByCap": "budget épuisé en raisonnement — pas de réponse", + "shell.uiStale": "La passerelle a été mise à jour — cette page utilise une ancienne interface.", + "shell.reload": "Recharger", }; diff --git a/web/js/i18n/ru.js b/web/js/i18n/ru.js index df0de9e..b00bc11 100644 --- a/web/js/i18n/ru.js +++ b/web/js/i18n/ru.js @@ -611,4 +611,6 @@ export default { "bm.degraded": "роутер деградировал", "bm.routerDegraded": "Роутер не успел ответить — запрос ушёл по резервной цепочке; классификация не засчитывается.", "bm.cutByCap": "бюджет ушёл на размышления — ответ не успел", + "shell.uiStale": "Шлюз обновился — на этой странице старый интерфейс.", + "shell.reload": "Перезагрузить", }; diff --git a/web/js/i18n/tr.js b/web/js/i18n/tr.js index 65cd0f3..a4154b8 100644 --- a/web/js/i18n/tr.js +++ b/web/js/i18n/tr.js @@ -619,4 +619,6 @@ export default { "bm.degraded": "yönlendirici yedeğe düştü", "bm.routerDegraded": "Yönlendirici zamanında yanıt vermedi — istek yedek zincire gitti; sınıflandırma sayılmaz.", "bm.cutByCap": "bütçe düşünmeye gitti — yanıt gelemedi", + "shell.uiStale": "Ağ geçidi güncellendi — bu sayfa eski arayüzü çalıştırıyor.", + "shell.reload": "Yeniden yükle", }; diff --git a/web/js/i18n/zh.js b/web/js/i18n/zh.js index 60afc1d..2aaf813 100644 --- a/web/js/i18n/zh.js +++ b/web/js/i18n/zh.js @@ -619,4 +619,6 @@ export default { "bm.degraded": "路由器降级", "bm.routerDegraded": "路由器未及时响应 — 请求走了备用链路;分类不计入统计。", "bm.cutByCap": "预算全用于思考——未能给出答案", + "shell.uiStale": "网关已更新 — 此页面运行的是旧界面。", + "shell.reload": "重新加载", };