improve website (#74)

makes the website look nicer and work better

- **add 404 page**
- **add website components**
- **add counter to keep track of connected clients**
- **add websocket stream for statistics**
This commit is contained in:
Noa Aarts 2024-12-15 10:15:38 +01:00 committed by GitHub
commit 5b53540e0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 148 additions and 13 deletions

20
assets/404.html Normal file
View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Flurry - OOPS</title>
<link href="/style.css" rel="stylesheet">
</head>
<body>
<div>
<h1>Oops</h1>
<p>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?</p>
<a href="/">go to the homepage</a>
</div>
</body>
</html>

View file

@ -5,11 +5,38 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Flurry</title> <title>Flurry</title>
<link href="css/style.css" rel="stylesheet"> <link href="/style.css" rel="stylesheet">
<script src="/stats.js"></script>
</head> </head>
<body> <body>
<img src="/imgstream?canvas=0" alt="The main pixelflut canvas"> <div>
<img class="grid" src="/imgstream?canvas=0" draggable="false" alt="Pixelflut canvas">
<table>
<thead>
<tr>
<th>Stat</th>
<th>Total</th>
<th>Last Second</th>
</tr>
</thead>
<tbody>
<tr>
<td>Pixels changed in main</td>
<td id="pixelCounter">Loading...</td>
<td id="pixelCounterAvg">Loading...</td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<td>Clients Connected right now</td>
<td id="clientCounter">Loading...</td>
</tr>
</tbody>
</table>
</div>
</body> </body>
</html> </html>

39
assets/stats.js Normal file
View file

@ -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);
};
};

30
assets/style.css Normal file
View file

@ -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;
}

View file

@ -21,6 +21,7 @@ pub type Canvas = u8;
pub type Coordinate = u16; pub type Coordinate = u16;
pub static COUNTER: AtomicU64 = AtomicU64::new(0); pub static COUNTER: AtomicU64 = AtomicU64::new(0);
pub static CLIENTS: AtomicU64 = AtomicU64::new(0);
pub type AsyncResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>; pub type AsyncResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;

View file

@ -12,7 +12,7 @@ use flurry::{
flutclient::{FlutClient, ParserTypes}, flutclient::{FlutClient, ParserTypes},
grid::{self, Flut}, grid::{self, Flut},
webapi::WebApiContext, webapi::WebApiContext,
AsyncResult, AsyncResult, CLIENTS,
}; };
use futures::never::Never; use futures::never::Never;
use tokio::{net::TcpListener, time::interval, try_join}; use tokio::{net::TcpListener, time::interval, try_join};
@ -59,11 +59,10 @@ async fn handle_flut(
handles.push(tokio::spawn(async move { handles.push(tokio::spawn(async move {
let (reader, writer) = socket.split(); let (reader, writer) = socket.split();
let mut connection = FlutClient::new(reader, writer, grids); let mut connection = FlutClient::new(reader, writer, grids);
CLIENTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let resp = connection.process_socket().await; let resp = connection.process_socket().await;
match resp { CLIENTS.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
Ok(()) => Ok(()), resp
Err(err) => Err(err),
}
})) }))
} }
} }

View file

@ -1,10 +1,10 @@
use std::{net::SocketAddr, process::exit, sync::Arc, time::Duration}; use std::{net::SocketAddr, process::exit, sync::Arc, time::Duration};
use axum::{ use axum::{
extract::{ConnectInfo, Query, State}, extract::{ws::Message, ConnectInfo, Query, State, WebSocketUpgrade},
http::{self, HeaderMap, HeaderValue}, http::{self, HeaderMap, HeaderValue},
response::IntoResponse, response::{IntoResponse, Response},
routing::any, routing::get,
Router, Router,
}; };
use axum_extra::TypedHeader; use axum_extra::TypedHeader;
@ -12,14 +12,14 @@ use axum_streams::StreamBodyAs;
use futures::{never::Never, stream::repeat_with, Stream}; use futures::{never::Never, stream::repeat_with, Stream};
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use serde::Deserialize; use serde::Deserialize;
use tokio::{net::TcpListener, time::sleep}; use tokio::{net::TcpListener, time::interval};
use tower_http::trace::{DefaultMakeSpan, TraceLayer}; use tower_http::trace::{DefaultMakeSpan, TraceLayer};
use crate::{ use crate::{
config::{WEB_HOST, WEB_UPDATE_INTERVAL}, config::{WEB_HOST, WEB_UPDATE_INTERVAL},
grid, grid,
stream::Multipart, stream::Multipart,
AsyncResult, AsyncResult, CLIENTS, COUNTER,
}; };
#[derive(RustEmbed, Clone)] #[derive(RustEmbed, Clone)]
@ -38,7 +38,8 @@ pub async fn serve(ctx: WebApiContext) -> AsyncResult<Never> {
Some("index.html".to_string()), Some("index.html".to_string()),
); );
let app = Router::new() let app = Router::new()
.route("/imgstream", any(image_stream)) .route("/imgstream", get(image_stream))
.route("/stats", get(stats_stream))
.nest_service("/", assets) .nest_service("/", assets)
.with_state(ctx) .with_state(ctx)
// logging middleware // logging middleware
@ -85,6 +86,24 @@ fn make_image_stream(
.throttle(WEB_UPDATE_INTERVAL) .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( async fn image_stream(
user_agent: Option<TypedHeader<headers::UserAgent>>, user_agent: Option<TypedHeader<headers::UserAgent>>,
ConnectInfo(addr): ConnectInfo<SocketAddr>, ConnectInfo(addr): ConnectInfo<SocketAddr>,