diff --git a/assets/404.html b/assets/404.html new file mode 100644 index 0000000..b1acec1 --- /dev/null +++ b/assets/404.html @@ -0,0 +1,20 @@ + + + + + + + Flurry - OOPS + + + + +
+

Oops

+

it seems like the page you were trying to get does not exist, + that's sad. But maybe you can find it again from the homepage?

+ go to the homepage +
+ + + diff --git a/assets/index.html b/assets/index.html index 02ebee2..62fadf6 100644 --- a/assets/index.html +++ b/assets/index.html @@ -5,11 +5,38 @@ Flurry - + + - The main pixelflut canvas +
+ Pixelflut canvas + + + + + + + + + + + + + + + +
StatTotalLast Second
Pixels changed in mainLoading...Loading...
+ + + + + + + +
Clients Connected right nowLoading...
+
diff --git a/assets/stats.js b/assets/stats.js new file mode 100644 index 0000000..dc63d78 --- /dev/null +++ b/assets/stats.js @@ -0,0 +1,39 @@ +const formatter = Intl.NumberFormat("en", { notation: "compact" }); +function nString(value) { + return formatter.format(value); +} + +window.onload = function() { + var client = document.getElementById("clientCounter"); + var pixel = document.getElementById("pixelCounter"); + var pixelAvg = document.getElementById("pixelCounterAvg"); + + var pixelQueue = []; + + for (i = 0; i < 5; i++) { + pixelQueue.push(0); + } + + const stats = new WebSocket("/stats"); + + stats.onopen = function() { + console.log("Connected to flut-stats."); + }; + stats.onerror = function(error) { + console.error("An unknown error occured", error); + }; + + stats.onclose = function(event) { + console.log("Server closed connection", event); + }; + + stats.onmessage = function(event) { + const obj = JSON.parse(event.data); + client.innerText = nString(obj.c); + + pixel.innerText = nString(obj.p); + pixelQueue.push(obj.p); + var old = pixelQueue.shift(); + pixelAvg.innerText = nString(obj.p - old); + }; +}; diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..c76baad --- /dev/null +++ b/assets/style.css @@ -0,0 +1,30 @@ +:root { + --background-primary: rgba(2, 0, 36, 1); + --background-secondary: rgba(0, 0, 0, 1); +} + +body { + background: var(--background-primary); + background: radial-gradient(circle, var(--background-primary) 0%, var(--background-secondary) 100%); + background-repeat: no-repeat; + background-attachment: fixed; + height: 100%; +} + +body>div { + max-width: fit-content; + margin-inline: auto; + background: #DDDDDD; + margin-top: 3rem; + padding: 0.75rem; + border-radius: 1.5rem; +} + +img.grid { + border-radius: 0.75rem; + max-width: 80vw; + min-width: 60vw; + height: auto; + image-rendering: pixelated; + user-select: none; +} diff --git a/src/lib.rs b/src/lib.rs index ca49c1f..bb16ec3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ pub type Canvas = u8; pub type Coordinate = u16; pub static COUNTER: AtomicU64 = AtomicU64::new(0); +pub static CLIENTS: AtomicU64 = AtomicU64::new(0); pub type AsyncResult = Result>; diff --git a/src/main.rs b/src/main.rs index 85fdfb8..47241bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use flurry::{ flutclient::{FlutClient, ParserTypes}, grid::{self, Flut}, webapi::WebApiContext, - AsyncResult, + AsyncResult, CLIENTS, }; use futures::never::Never; use tokio::{net::TcpListener, time::interval, try_join}; @@ -59,11 +59,10 @@ async fn handle_flut( handles.push(tokio::spawn(async move { let (reader, writer) = socket.split(); let mut connection = FlutClient::new(reader, writer, grids); + CLIENTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let resp = connection.process_socket().await; - match resp { - Ok(()) => Ok(()), - Err(err) => Err(err), - } + CLIENTS.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + resp })) } } diff --git a/src/webapi.rs b/src/webapi.rs index d5c2e60..1680b1b 100644 --- a/src/webapi.rs +++ b/src/webapi.rs @@ -1,10 +1,10 @@ use std::{net::SocketAddr, process::exit, sync::Arc, time::Duration}; use axum::{ - extract::{ConnectInfo, Query, State}, + extract::{ws::Message, ConnectInfo, Query, State, WebSocketUpgrade}, http::{self, HeaderMap, HeaderValue}, - response::IntoResponse, - routing::any, + response::{IntoResponse, Response}, + routing::get, Router, }; use axum_extra::TypedHeader; @@ -12,14 +12,14 @@ use axum_streams::StreamBodyAs; use futures::{never::Never, stream::repeat_with, Stream}; use rust_embed::RustEmbed; use serde::Deserialize; -use tokio::{net::TcpListener, time::sleep}; +use tokio::{net::TcpListener, time::interval}; use tower_http::trace::{DefaultMakeSpan, TraceLayer}; use crate::{ config::{WEB_HOST, WEB_UPDATE_INTERVAL}, grid, stream::Multipart, - AsyncResult, + AsyncResult, CLIENTS, COUNTER, }; #[derive(RustEmbed, Clone)] @@ -38,7 +38,8 @@ pub async fn serve(ctx: WebApiContext) -> AsyncResult { Some("index.html".to_string()), ); let app = Router::new() - .route("/imgstream", any(image_stream)) + .route("/imgstream", get(image_stream)) + .route("/stats", get(stats_stream)) .nest_service("/", assets) .with_state(ctx) // logging middleware @@ -85,6 +86,24 @@ fn make_image_stream( .throttle(WEB_UPDATE_INTERVAL) } +fn make_stats() -> Message { + let pixels: u64 = COUNTER.load(std::sync::atomic::Ordering::Relaxed); + let clients: u64 = CLIENTS.load(std::sync::atomic::Ordering::Relaxed); + format!("{{\"c\":{}, \"p\":{}}}", clients, pixels).into() +} + +async fn stats_stream(ws: WebSocketUpgrade) -> Response { + ws.on_upgrade(|mut c| async move { + let mut interval = interval(Duration::from_millis(100)); + loop { + interval.tick().await; + if let Err(e) = c.send(make_stats()).await { + tracing::warn!("websocket disconnected with {e:?}") + } + } + }) +} + async fn image_stream( user_agent: Option>, ConnectInfo(addr): ConnectInfo,