fix(shell): stale tabs now know the gateway was updated
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.
This commit is contained in:
@@ -600,8 +600,18 @@ async fn provider_models(
|
||||
}
|
||||
|
||||
async fn meta(State(state): State<SharedState>) -> 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<u64> = 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
|
||||
|
||||
Vendored
+18
@@ -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");
|
||||
|
||||
Vendored
+2
@@ -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",
|
||||
};
|
||||
|
||||
Vendored
+2
@@ -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",
|
||||
};
|
||||
|
||||
Vendored
+2
@@ -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",
|
||||
};
|
||||
|
||||
Vendored
+2
@@ -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",
|
||||
};
|
||||
|
||||
Vendored
+2
@@ -611,4 +611,6 @@ export default {
|
||||
"bm.degraded": "роутер деградировал",
|
||||
"bm.routerDegraded": "Роутер не успел ответить — запрос ушёл по резервной цепочке; классификация не засчитывается.",
|
||||
"bm.cutByCap": "бюджет ушёл на размышления — ответ не успел",
|
||||
"shell.uiStale": "Шлюз обновился — на этой странице старый интерфейс.",
|
||||
"shell.reload": "Перезагрузить",
|
||||
};
|
||||
|
||||
Vendored
+2
@@ -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",
|
||||
};
|
||||
|
||||
Vendored
+2
@@ -619,4 +619,6 @@ export default {
|
||||
"bm.degraded": "路由器降级",
|
||||
"bm.routerDegraded": "路由器未及时响应 — 请求走了备用链路;分类不计入统计。",
|
||||
"bm.cutByCap": "预算全用于思考——未能给出答案",
|
||||
"shell.uiStale": "网关已更新 — 此页面运行的是旧界面。",
|
||||
"shell.reload": "重新加载",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user