aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-30 21:45:22 +0100
committertv <tv@krebsco.de>2026-10-30 21:45:22 +0100
commitdb6e1ad6592aefce8a99db8478fac0baae09b228 (patch)
tree7f7f133eba4dbc5e8bc18effab81868cf16dc75f /src
parent114c9cb6f45abedb9b90ef6c9194570ff84fbda2 (diff)
dashboard: click on live/reconnecting triggers
Diffstat (limited to 'src')
-rw-r--r--src/api/dashboard/dashboard.html23
-rw-r--r--src/api/dashboard/mod.rs24
2 files changed, 42 insertions, 5 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index b3f8986..c9aaf4c 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -965,7 +965,11 @@ function status(text, live) {
// While not live, what is shown stays as it was last (ages don't count on).
let live = false;
let events = null;
+// The next attempt, while one is waited for: a connect now replaces it.
+let retry = null;
function connect() {
+ clearTimeout(retry);
+ retry = null;
events = new EventSource("dashboard/events");
events.addEventListener("snapshot", event => {
const snapshot = JSON.parse(event.data);
@@ -989,7 +993,7 @@ function connect() {
events.addEventListener("offline", () => {
events.close(); live = false;
status("node stopped; reconnecting…", false);
- setTimeout(connect, 3000);
+ retry = setTimeout(connect, 3000);
});
events.addEventListener("error", () => {
// Each attempt that failed: a breath too, so it shows it keeps trying.
@@ -998,7 +1002,7 @@ function connect() {
if (events.readyState === EventSource.CLOSED) {
// Given up (an HTTP error, e.g. the node is up but not serving the page yet).
status("disconnected; retrying…", false);
- setTimeout(connect, 5000);
+ retry = setTimeout(connect, 5000);
} else {
// EventSource reconnects by itself; the node sends a snapshot first again.
status("reconnecting…", false);
@@ -1006,6 +1010,21 @@ function connect() {
});
}
connect();
+// A click on the status: live, everything now (a tick on the node, the members asked
+// again, what's open of the document read again); otherwise, reconnecting now.
+connection.title = "Click to refresh now";
+connection.style.cursor = "pointer";
+connection.addEventListener("click", () => {
+ pulse();
+ if (live) {
+ fetch("dashboard/refresh", { method: "POST" }).catch(() => {});
+ documentChanged({ all: true });
+ } else {
+ if (events) events.close();
+ status("reconnecting…", false);
+ connect();
+ }
+});
// Ages count on between events; nothing else changes without one.
setInterval(() => { if (live) render(true); }, 1000);
window.addEventListener("resize", () => { lastShape = ""; render(); });
diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs
index 7679787..dd454ec 100644
--- a/src/api/dashboard/mod.rs
+++ b/src/api/dashboard/mod.rs
@@ -21,10 +21,10 @@ use std::time::Duration;
use axum::http::{HeaderValue, StatusCode, header::CONTENT_TYPE};
use axum::response::sse::{Event, KeepAlive, Sse};
use axum::response::{Html, IntoResponse, Response};
-use axum::routing::get;
+use axum::routing::{get, post};
use axum::{Extension, Router};
use serde_json::{Map, Value as JsonValue, json};
-use tokio::sync::{broadcast, watch};
+use tokio::sync::{Notify, broadcast, watch};
use tokio::time::Instant;
use crate::locks::MutexExt;
@@ -60,6 +60,8 @@ struct Hub {
/// `{"paths": [...]}`, or `{"all": true}` if too many or not known.
documents: broadcast::Sender<Arc<String>>,
control: Mutex<Control>,
+ /// Someone asked for it now (`refresh`): a tick at once, members asked again.
+ poke: Notify,
/// True once the node stops: every client gets an `offline` event, and its stream
/// ends (so the HTTP server needn't wait for clients to go).
closing: watch::Receiver<bool>,
@@ -81,6 +83,7 @@ impl Hub {
latest,
patches,
documents,
+ poke: Notify::new(),
control: Mutex::new(Control {
running: false,
wanted_until: Instant::now(),
@@ -151,7 +154,13 @@ impl Hub {
})));
last = Some(snapshot);
}
- tokio::time::sleep(TICK).await;
+ let poked = tokio::select! {
+ () = tokio::time::sleep(TICK) => false,
+ () = self.poke.notified() => true,
+ };
+ if poked {
+ graph_due = Instant::now();
+ }
if let Some(changed) = document_changes(&mut changes) {
let _ = self.documents.send(Arc::new(changed.to_string()));
}
@@ -360,6 +369,7 @@ pub(crate) fn router(state: AppState, closing: watch::Receiver<bool>) -> Router<
.route("/admin/dashboard", get(page_html))
.route("/admin/dashboard/state", get(current_state))
.route("/admin/dashboard/events", get(events))
+ .route("/admin/dashboard/refresh", post(refresh))
.layer(Extension(Hub::new(state, closing)))
}
@@ -389,6 +399,14 @@ async fn page_html() -> Html<&'static str> {
Html(page().1.as_str())
}
+/// `POST /admin/dashboard/refresh`: a tick now rather than at the next `TICK`, and the
+/// members asked for their links again; what changed comes as events, as ever.
+async fn refresh(Extension(hub): Extension<Arc<Hub>>) -> StatusCode {
+ hub.want();
+ hub.poke.notify_one();
+ StatusCode::NO_CONTENT
+}
+
/// `GET /admin/dashboard/state`: the latest snapshot, for a client that wants it whole.
async fn current_state(Extension(hub): Extension<Arc<Hub>>) -> Response {
hub.want();