From 0d831698e2aa85a81690baa03442dfde5ed17067 Mon Sep 17 00:00:00 2001 From: liquidev Date: Sun, 25 Aug 2024 12:53:53 +0200 Subject: [PATCH] add proper error and disconnect handling error handling shows you the error and offers the ability to reload; disconnect handling shows you that the page will reload in a few seconds. it uses exponential backoff with some random sprinkled into it to prevent overwhelming the server once people's clients decide to reconnect. --- crates/rkgk/src/live_reload.rs | 13 ++++++--- crates/rkgk/src/main.rs | 4 +-- static/connection-status.js | 48 ++++++++++++++++++++++++++++++++++ static/index.css | 30 ++++++++++++++++++--- static/index.html | 29 +++++++++++++++++++- static/index.js | 33 ++++++++++++++++++----- static/live-reload.js | 4 +-- static/session.js | 9 ++++++- 8 files changed, 150 insertions(+), 20 deletions(-) diff --git a/crates/rkgk/src/live_reload.rs b/crates/rkgk/src/live_reload.rs index 909b0b9..fe079ec 100644 --- a/crates/rkgk/src/live_reload.rs +++ b/crates/rkgk/src/live_reload.rs @@ -4,12 +4,17 @@ use axum::{routing::get, Router}; use tokio::time::sleep; pub fn router() -> Router { - Router::new() - .route("/stall", get(stall)) - .route("/back-up", get(back_up)) - .with_state(()) + let router = Router::new().route("/back-up", get(back_up)); + + // The endpoint for immediate reload is only enabled on debug builds. + // Release builds use the exponential backoff system that detects is the WebSocket is closed. + #[cfg(debug_assertions)] + let router = router.route("/stall", get(stall)); + + router.with_state(()) } +#[cfg(debug_assertions)] async fn stall() -> String { loop { // Sleep for a day, I guess. Just to uphold the connection forever without really using any diff --git a/crates/rkgk/src/main.rs b/crates/rkgk/src/main.rs index b322b18..bcefa95 100644 --- a/crates/rkgk/src/main.rs +++ b/crates/rkgk/src/main.rs @@ -18,7 +18,6 @@ mod api; mod config; mod haku; mod id; -#[cfg(debug_assertions)] mod live_reload; mod login; pub mod schema; @@ -102,8 +101,7 @@ async fn fallible_main() -> eyre::Result<()> { .nest_service("/static", ServeDir::new(paths.target_dir.join("static"))) .nest("/api", api::router(api)); - #[cfg(debug_assertions)] - let app = app.nest("/dev/live-reload", live_reload::router()); + let app = app.nest("/auto-reload", live_reload::router()); let port: u16 = std::env::var("RKGK_PORT") .unwrap_or("8080".into()) diff --git a/static/connection-status.js b/static/connection-status.js index 2162d51..e563b04 100644 --- a/static/connection-status.js +++ b/static/connection-status.js @@ -5,6 +5,25 @@ export class ConnectionStatus extends HTMLElement { // This is a progress dialog and shouldn't be closed. this.loggingInDialog.addEventListener("cancel", (event) => event.preventDefault()); + + this.errorDialog = this.querySelector("dialog[name='error-dialog']"); + this.errorText = this.errorDialog.querySelector("[name='error-text']"); + this.errorRefresh = this.errorDialog.querySelector("button[name='refresh']"); + + // If this appears then something broke, and therefore the app can't continue normally. + this.errorDialog.addEventListener("cancel", (event) => event.preventDefault()); + + this.errorRefresh.addEventListener("click", () => { + window.location.reload(); + }); + + this.disconnectedDialog = this.querySelector("dialog[name='disconnected-dialog']"); + this.reconnectDuration = this.disconnectedDialog.querySelector( + "[name='reconnect-duration']", + ); + + // If this appears then we can't let the user use the app, because we're disconnected. + this.disconnectedDialog.addEventListener("cancel", (event) => event.preventDefault()); } showLoggingIn() { @@ -14,6 +33,35 @@ export class ConnectionStatus extends HTMLElement { hideLoggingIn() { this.loggingInDialog.close(); } + + showError(error) { + this.errorDialog.showModal(); + if (error instanceof Error) { + if (error.stack != null && error.stack != "") { + this.errorText.textContent = `${error.toString()}\n\n${error.stack}`; + } else { + this.errorText.textContent = error.toString(); + } + } + } + + async showDisconnected(duration) { + this.disconnectedDialog.showModal(); + + let updateDuration = (remaining) => { + let seconds = Math.floor(remaining / 1000); + this.reconnectDuration.textContent = `${seconds} ${seconds == 1 ? "second" : "seconds"}`; + }; + + let remaining = duration; + updateDuration(remaining); + while (remaining > 0) { + let delay = Math.min(1000, remaining); + remaining -= delay; + updateDuration(remaining); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } } customElements.define("rkgk-connection-status", ConnectionStatus); diff --git a/static/index.css b/static/index.css index 4a656ff..b18a30f 100644 --- a/static/index.css +++ b/static/index.css @@ -36,6 +36,15 @@ body { font-weight: 400; } +@font-face { + font-family: "Fira Sans"; + src: + local("Fira Sans Italic"), + url("font/FiraSans-Italic.ttf"); + font-weight: 400; + font-style: italic; +} + @font-face { font-family: "Fira Sans"; src: @@ -131,8 +140,7 @@ input { border-bottom: 1px solid var(--color-panel-border); } -*[contenteditable]:focus, input:focus, textarea:focus { - border-radius: 2px; +*:focus { outline: 1px solid #40b1f4; outline-offset: 4px; } @@ -149,6 +157,12 @@ dialog::backdrop { backdrop-filter: blur(8px); } +/* Details */ + +details>summary { + cursor: pointer; +} + /* Throbbers */ @keyframes rkgk-throbber-loading { @@ -295,7 +309,7 @@ rkgk-welcome { /* Connection status dialogs */ rkgk-connection-status { - &>dialog[name='logging-in-dialog'][open] { + &>dialog[name='logging-in-dialog'][open], &>dialog[name='disconnected-dialog'][open] { border: none; outline: none; background: none; @@ -304,4 +318,14 @@ rkgk-connection-status { gap: 8px; align-items: center; } + + &>dialog[name='error-dialog'][open] { + & textarea[name='error-text'] { + box-sizing: border-box; + width: 100%; + resize: none; + border: 1px solid var(--color-panel-border); + padding: 4px; + } + } } diff --git a/static/index.html b/static/index.html index 20f698e..14c899b 100644 --- a/static/index.html +++ b/static/index.html @@ -55,12 +55,38 @@ -

Logging in...

+

Logging in…

+
+ + +

owie! >_<

+

Uh oh. Seems like the pipe cracked again… There's water everywhere.
The basement's half full already. God dammit.

+ +

Super sorry about this! But rakugaki encountered an error and has to restart.

+

Rest assured your drawings are safe and sound.

+

Either way… try refreshing the page and see if it helps. If not, please report a bug with the following details.

+ +
+ Show error details + +
+ +

Thank you from the mountain!

+ +
+ +
+
+ + + +

Connection lost. Attempting to reconnect in

+