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:
commit
5b53540e0c
7 changed files with 148 additions and 13 deletions
20
assets/404.html
Normal file
20
assets/404.html
Normal 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>
|
||||||
|
|
@ -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
39
assets/stats.js
Normal 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
30
assets/style.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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>>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue