diff options
Diffstat (limited to 'src/api/dashboard')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 21 | ||||
| -rw-r--r-- | src/api/dashboard/mod.rs | 30 |
2 files changed, 45 insertions, 6 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index 11e53d2..a5181ca 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -113,6 +113,9 @@ </main> <script> "use strict"; +// Which page this is (the node fills it in); a snapshot naming another came from +// another build: reloaded then, to get its page. +const PAGE_VERSION = "__PAGE_VERSION__"; // The state as the node published it, and when it did (the last event). let state = null; let stateAt = 0; @@ -167,11 +170,18 @@ function age(secs) { // What is in focus, its figures shown: a node's ID, or a link's pair key (`pairKey`, // two IDs). null: this node's (`/admin/stats`). -let focused = null; +let focused = (() => { + try { return decodeURIComponent(location.hash.slice(1)) || null; } catch { return null; } +})(); const pairKey = (a, b) => a < b ? a + " " + b : b + " " + a; const isPair = key => key !== null && key.includes(" "); function focus(id) { focused = id === null || id === state.node.id ? null : id; + // In the URL too, so a reload (a new version) keeps it. + try { + history.replaceState(null, "", focused === null ? location.pathname + : "#" + encodeURIComponent(focused)); + } catch { /* not where history works */ } render(); } @@ -536,7 +546,14 @@ let events = null; function connect() { events = new EventSource("dashboard/events"); events.addEventListener("snapshot", event => { - state = JSON.parse(event.data); stateAt = Date.now(); live = true; + const snapshot = JSON.parse(event.data); + if (snapshot.page !== PAGE_VERSION) { + events.close(); + status("new version; reloading…", false); + location.reload(); + return; + } + state = snapshot; stateAt = Date.now(); live = true; status("live", true); render(); }); events.addEventListener("patch", event => { diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs index 1de21cd..99a0b83 100644 --- a/src/api/dashboard/mod.rs +++ b/src/api/dashboard/mod.rs @@ -15,7 +15,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::convert::Infallible; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; use std::time::Duration; use axum::http::{HeaderValue, StatusCode, header::CONTENT_TYPE}; @@ -221,6 +221,7 @@ async fn build(state: &AppState, graph: &Graph) -> JsonValue { } json!({ "node": { "id": local, "alias": state.alias }, + "page": page().0, "stats": stats, "peers": peers, "graph": { "members": members_json, "intended": intended, "measured": measured }, @@ -312,15 +313,36 @@ fn merge_patch(old: &JsonValue, new: &JsonValue) -> Option<JsonValue> { /// `closing` turns true when the node stops (`Hub::closing`). pub(crate) fn router(state: AppState, closing: watch::Receiver<bool>) -> Router<AppState> { Router::new() - .route("/admin/dashboard", get(page)) + .route("/admin/dashboard", get(page_html)) .route("/admin/dashboard/state", get(current_state)) .route("/admin/dashboard/events", get(events)) .layer(Extension(Hub::new(state, closing))) } +/// The page as built into this binary. +const PAGE: &str = include_str!("dashboard.html"); +/// Where the page says which version of it it is. +const PAGE_VERSION_MARK: &str = "__PAGE_VERSION__"; + +/// The page's version (a hash of it), and the page saying so: a page whose version +/// differs from the snapshot's `page` was served by another build, and reloads. +fn page() -> &'static (String, String) { + static PAGE_SERVED: OnceLock<(String, String)> = OnceLock::new(); + PAGE_SERVED.get_or_init(|| { + use sha2::{Digest, Sha256}; + let version: String = Sha256::digest(PAGE.as_bytes()) + .iter() + .take(8) + .map(|byte| format!("{byte:02x}")) + .collect(); + let served = PAGE.replace(PAGE_VERSION_MARK, &version); + (version, served) + }) +} + /// `GET /admin/dashboard`: the page. -async fn page() -> Html<&'static str> { - Html(include_str!("dashboard.html")) +async fn page_html() -> Html<&'static str> { + Html(page().1.as_str()) } /// `GET /admin/dashboard/state`: the latest snapshot, for a client that wants it whole. |
