mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-08 13:13:49 -05:00
Changed frontend from Liquid to React, fixed bug where server would check for updates twice
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
/docs/.next
|
/docs/.next
|
||||||
/docs/node_modules
|
/docs/node_modules
|
||||||
/docs/out
|
/docs/out
|
||||||
|
/src/static
|
||||||
|
|
||||||
# In case I accidentally commit mine...
|
# In case I accidentally commit mine...
|
||||||
cup.json
|
cup.json
|
||||||
152
Cargo.lock
generated
152
Cargo.lock
generated
@@ -90,12 +90,6 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anymap2"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atty"
|
name = "atty"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
@@ -358,7 +352,6 @@ dependencies = [
|
|||||||
"http-auth",
|
"http-auth",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"json",
|
"json",
|
||||||
"liquid",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rayon",
|
"rayon",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -388,12 +381,6 @@ dependencies = [
|
|||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "doc-comment"
|
|
||||||
version = "0.3.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
@@ -768,15 +755,6 @@ version = "1.70.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
@@ -798,16 +776,6 @@ version = "0.12.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd"
|
checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "kstring"
|
|
||||||
version = "2.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"static_assertions",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -820,63 +788,6 @@ version = "0.2.155"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "liquid"
|
|
||||||
version = "0.26.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "10929f201279ba14da3297b957dcda1e0bf7a6f3bb5115688be684aa8864e9cc"
|
|
||||||
dependencies = [
|
|
||||||
"doc-comment",
|
|
||||||
"liquid-core",
|
|
||||||
"liquid-derive",
|
|
||||||
"liquid-lib",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "liquid-core"
|
|
||||||
version = "0.26.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3aef4b2160791f456eb880c990a97746f693746f92302ef5f1d06111cf14b768"
|
|
||||||
dependencies = [
|
|
||||||
"anymap2",
|
|
||||||
"itertools",
|
|
||||||
"kstring",
|
|
||||||
"liquid-derive",
|
|
||||||
"num-traits",
|
|
||||||
"pest",
|
|
||||||
"pest_derive",
|
|
||||||
"regex",
|
|
||||||
"serde",
|
|
||||||
"time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "liquid-derive"
|
|
||||||
version = "0.26.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "915f6d0a2963a27cd5205c1902f32ddfe3bc035816afd268cf88c0fc0f8d287e"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "liquid-lib"
|
|
||||||
version = "0.26.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "73f48fc446873f74d869582f5c4b8cbf3248c93395e410a67af5809b3731e44a"
|
|
||||||
dependencies = [
|
|
||||||
"itertools",
|
|
||||||
"liquid-core",
|
|
||||||
"once_cell",
|
|
||||||
"percent-encoding",
|
|
||||||
"regex",
|
|
||||||
"time",
|
|
||||||
"unicode-segmentation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.22"
|
version = "0.4.22"
|
||||||
@@ -987,51 +898,6 @@ version = "2.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pest"
|
|
||||||
version = "2.7.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"thiserror",
|
|
||||||
"ucd-trie",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pest_derive"
|
|
||||||
version = "2.7.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a"
|
|
||||||
dependencies = [
|
|
||||||
"pest",
|
|
||||||
"pest_generator",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pest_generator"
|
|
||||||
version = "2.7.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183"
|
|
||||||
dependencies = [
|
|
||||||
"pest",
|
|
||||||
"pest_meta",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pest_meta"
|
|
||||||
version = "2.7.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
"pest",
|
|
||||||
"sha2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.5"
|
version = "1.1.5"
|
||||||
@@ -1369,12 +1235,6 @@ version = "0.9.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "static_assertions"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -1623,12 +1483,6 @@ version = "1.17.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ucd-trie"
|
|
||||||
version = "0.1.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.15"
|
version = "0.3.15"
|
||||||
@@ -1650,12 +1504,6 @@ dependencies = [
|
|||||||
"tinyvec",
|
"tinyvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-segmentation"
|
|
||||||
version = "1.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ tokio = {version = "1.38.0", features = ["rt", "rt-multi-thread", "macros"]}
|
|||||||
ureq = { version = "2.9.7", features = ["tls"] }
|
ureq = { version = "2.9.7", features = ["tls"] }
|
||||||
rayon = "1.10.0"
|
rayon = "1.10.0"
|
||||||
xitca-web = { version = "0.5.0", optional = true, features = ["logger"] }
|
xitca-web = { version = "0.5.0", optional = true, features = ["logger"] }
|
||||||
liquid = { version = "0.26.6", optional = true }
|
|
||||||
bollard = "0.16.1"
|
bollard = "0.16.1"
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
http-auth = { version = "0.1.9", features = [] }
|
http-auth = { version = "0.1.9", features = [] }
|
||||||
@@ -21,7 +20,7 @@ json = "0.12.4"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["server", "cli"]
|
default = ["server", "cli"]
|
||||||
server = ["dep:xitca-web", "dep:liquid", "dep:chrono"]
|
server = ["dep:xitca-web", "dep:chrono"]
|
||||||
cli = ["dep:indicatif", "dep:termsize"]
|
cli = ["dep:indicatif", "dep:termsize"]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
7
build.sh
Executable file
7
build.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
rm -rf src/static
|
||||||
|
cd web/
|
||||||
|
bun run build
|
||||||
|
cp -r dist/ ../src/static
|
||||||
|
cargo build $@
|
||||||
@@ -2,11 +2,10 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use json::JsonValue;
|
use json::JsonValue;
|
||||||
use liquid::{object, Object};
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use xitca_web::{
|
use xitca_web::{
|
||||||
body::ResponseBody,
|
body::ResponseBody,
|
||||||
handler::{handler_service, state::StateRef},
|
handler::{handler_service, path::PathRef, state::StateRef},
|
||||||
http::WebResponse,
|
http::WebResponse,
|
||||||
middleware::Logger,
|
middleware::Logger,
|
||||||
route::get,
|
route::get,
|
||||||
@@ -19,23 +18,22 @@ use crate::{
|
|||||||
utils::{sort_update_vec, to_json},
|
utils::{sort_update_vec, to_json},
|
||||||
};
|
};
|
||||||
|
|
||||||
const RAW_TEMPLATE: &str = include_str!("static/template.liquid");
|
const HTML: &str = include_str!("static/index.html");
|
||||||
const STYLE: &str = include_str!("static/index.css");
|
const JS: &str = include_str!("static/assets/index.js");
|
||||||
|
const CSS: &str = include_str!("static/assets/index.css");
|
||||||
const FAVICON_ICO: &[u8] = include_bytes!("static/favicon.ico");
|
const FAVICON_ICO: &[u8] = include_bytes!("static/favicon.ico");
|
||||||
const FAVICON_SVG: &[u8] = include_bytes!("static/favicon.svg");
|
const FAVICON_SVG: &[u8] = include_bytes!("static/favicon.svg");
|
||||||
const APPLE_TOUCH_ICON: &[u8] = include_bytes!("static/apple-touch-icon.png");
|
const APPLE_TOUCH_ICON: &[u8] = include_bytes!("static/apple-touch-icon.png");
|
||||||
|
|
||||||
pub async fn serve(port: &u16, socket: Option<String>, config: JsonValue) -> std::io::Result<()> {
|
pub async fn serve(port: &u16, socket: Option<String>, config: JsonValue) -> std::io::Result<()> {
|
||||||
let mut data = ServerData::new(socket, config).await;
|
println!("Starting server, please wait...");
|
||||||
data.refresh().await;
|
let data = ServerData::new(socket, config).await;
|
||||||
App::new()
|
App::new()
|
||||||
.with_state(Arc::new(Mutex::new(data)))
|
.with_state(Arc::new(Mutex::new(data)))
|
||||||
.at("/", get(handler_service(home)))
|
.at("/", get(handler_service(_static)))
|
||||||
.at("/json", get(handler_service(json)))
|
.at("/json", get(handler_service(json)))
|
||||||
.at("/refresh", get(handler_service(refresh)))
|
.at("/refresh", get(handler_service(refresh)))
|
||||||
.at("/favicon.ico", handler_service(favicon_ico)) // These aren't pretty but this is xitca-web...
|
.at("/*", get(handler_service(_static)))
|
||||||
.at("/favicon.svg", handler_service(favicon_svg))
|
|
||||||
.at("/apple-touch-icon.png", handler_service(apple_touch_icon))
|
|
||||||
.enclosed(Logger::new())
|
.enclosed(Logger::new())
|
||||||
.serve()
|
.serve()
|
||||||
.bind(format!("0.0.0.0:{}", port))?
|
.bind(format!("0.0.0.0:{}", port))?
|
||||||
@@ -43,8 +41,16 @@ pub async fn serve(port: &u16, socket: Option<String>, config: JsonValue) -> std
|
|||||||
.wait()
|
.wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn home(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
async fn _static(data: StateRef<'_, Arc<Mutex<ServerData>>>, path: PathRef<'_>) -> WebResponse {
|
||||||
WebResponse::new(ResponseBody::from(data.lock().await.template.clone()))
|
match path.0 {
|
||||||
|
"/" => WebResponse::builder().header("Content-Type", "text/html").body(ResponseBody::from(HTML)).unwrap(),
|
||||||
|
"/assets/index.js" => WebResponse::builder().header("Content-Type", "text/javascript").body(ResponseBody::from(JS.replace("=\"neutral\"", &format!("=\"{}\"", data.lock().await.theme)))).unwrap(),
|
||||||
|
"/assets/index.css" => WebResponse::builder().header("Content-Type", "text/css").body(ResponseBody::from(CSS)).unwrap(),
|
||||||
|
"/favicon.ico" => WebResponse::builder().header("Content-Type", "image/vnd.microsoft.icon").body(ResponseBody::from(FAVICON_ICO)).unwrap(),
|
||||||
|
"/favicon.svg" => WebResponse::builder().header("Content-Type", "image/svg+xml").body(ResponseBody::from(FAVICON_SVG)).unwrap(),
|
||||||
|
"/apple-touch-icon.png" => WebResponse::builder().header("Content-Type", "image/png").body(ResponseBody::from(APPLE_TOUCH_ICON)).unwrap(),
|
||||||
|
_ => WebResponse::builder().status(404).body(ResponseBody::from("Not found")).unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn json(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
async fn json(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
||||||
@@ -58,37 +64,25 @@ async fn refresh(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
|||||||
WebResponse::new(ResponseBody::from("OK"))
|
WebResponse::new(ResponseBody::from("OK"))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn favicon_ico() -> WebResponse {
|
|
||||||
WebResponse::new(ResponseBody::from(FAVICON_ICO))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn favicon_svg() -> WebResponse {
|
|
||||||
WebResponse::new(ResponseBody::from(FAVICON_SVG))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn apple_touch_icon() -> WebResponse {
|
|
||||||
WebResponse::new(ResponseBody::from(APPLE_TOUCH_ICON))
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ServerData {
|
struct ServerData {
|
||||||
template: String,
|
|
||||||
raw_updates: Vec<(String, Option<bool>)>,
|
raw_updates: Vec<(String, Option<bool>)>,
|
||||||
json: JsonValue,
|
json: JsonValue,
|
||||||
socket: Option<String>,
|
socket: Option<String>,
|
||||||
config: JsonValue,
|
config: JsonValue,
|
||||||
|
theme: &'static str
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerData {
|
impl ServerData {
|
||||||
async fn new(socket: Option<String>, config: JsonValue) -> Self {
|
async fn new(socket: Option<String>, config: JsonValue) -> Self {
|
||||||
let mut s = Self {
|
let mut s = Self {
|
||||||
socket,
|
socket,
|
||||||
template: String::new(),
|
|
||||||
json: json::object! {
|
json: json::object! {
|
||||||
metrics: json::object! {},
|
metrics: json::object! {},
|
||||||
images: json::object! {},
|
images: json::object! {},
|
||||||
},
|
},
|
||||||
raw_updates: Vec::new(),
|
raw_updates: Vec::new(),
|
||||||
config,
|
config,
|
||||||
|
theme: "neutral"
|
||||||
};
|
};
|
||||||
s.refresh().await;
|
s.refresh().await;
|
||||||
s
|
s
|
||||||
@@ -96,22 +90,10 @@ impl ServerData {
|
|||||||
async fn refresh(&mut self) {
|
async fn refresh(&mut self) {
|
||||||
let updates = sort_update_vec(&get_all_updates(self.socket.clone(), &self.config["authentication"]).await);
|
let updates = sort_update_vec(&get_all_updates(self.socket.clone(), &self.config["authentication"]).await);
|
||||||
self.raw_updates = updates;
|
self.raw_updates = updates;
|
||||||
let template = liquid::ParserBuilder::with_stdlib()
|
|
||||||
.build()
|
|
||||||
.unwrap()
|
|
||||||
.parse(RAW_TEMPLATE)
|
|
||||||
.unwrap();
|
|
||||||
let images = self
|
|
||||||
.raw_updates
|
|
||||||
.iter()
|
|
||||||
.map(|(name, has_update)| match has_update {
|
|
||||||
Some(v) => object!({"name": name, "has_update": v.to_string()}), // Liquid kinda thinks false == nil, so we'll be comparing strings from now on
|
|
||||||
None => object!({"name": name, "has_update": "null"}),
|
|
||||||
})
|
|
||||||
.collect::<Vec<Object>>();
|
|
||||||
self.json = to_json(&self.raw_updates);
|
self.json = to_json(&self.raw_updates);
|
||||||
let last_updated = Local::now().format("%Y-%m-%d %H:%M:%S");
|
let last_updated = Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||||
let theme = match &self.config["theme"].as_str() {
|
self.json["last_updated"] = last_updated.to_string().into();
|
||||||
|
self.theme = match &self.config["theme"].as_str() {
|
||||||
Some(t) => match *t {
|
Some(t) => match *t {
|
||||||
"default" => "neutral",
|
"default" => "neutral",
|
||||||
"blue" => "gray",
|
"blue" => "gray",
|
||||||
@@ -122,13 +104,5 @@ impl ServerData {
|
|||||||
},
|
},
|
||||||
None => "neutral",
|
None => "neutral",
|
||||||
};
|
};
|
||||||
let globals = object!({
|
|
||||||
"metrics": [{"name": "Monitored images", "value": self.json["metrics"]["monitored_images"].as_usize()}, {"name": "Up to date", "value": self.json["metrics"]["up_to_date"].as_usize()}, {"name": "Updates available", "value": self.json["metrics"]["update_available"].as_usize()}, {"name": "Unknown", "value": self.json["metrics"]["unknown"].as_usize()}],
|
|
||||||
"images": images,
|
|
||||||
"style": STYLE,
|
|
||||||
"last_updated": last_updated.to_string(),
|
|
||||||
"theme": theme
|
|
||||||
});
|
|
||||||
self.template = template.render(&globals).unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,294 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf8">
|
|
||||||
<meta name="theme-color" content="#ffffff">
|
|
||||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
||||||
<link rel="apple-touch-icon" href="apple-touch-icon.png">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<title>Cup</title>
|
|
||||||
<style>
|
|
||||||
{{ style }}
|
|
||||||
</style>
|
|
||||||
<style>
|
|
||||||
/* Kinda hacky, but thank you https://geary.co/internal-borders-css-grid/ */
|
|
||||||
.gi {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gi::before,
|
|
||||||
.gi::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
{% if theme == "neutral" %}
|
|
||||||
background-color: #e5e5e5;
|
|
||||||
{% elsif theme == "gray" %}
|
|
||||||
background-color: #e5e7eb
|
|
||||||
{% endif %}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.gi::before,
|
|
||||||
.gi::after {
|
|
||||||
{% if theme == "neutral" %}
|
|
||||||
background-color: #262626;
|
|
||||||
{% elsif theme == "gray" %}
|
|
||||||
background-color: #1f2937
|
|
||||||
{% endif %}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gi::before {
|
|
||||||
inline-size: 1px;
|
|
||||||
block-size: 100vh;
|
|
||||||
inset-inline-start: -0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gi::after {
|
|
||||||
inline-size: 100vw;
|
|
||||||
block-size: 1px;
|
|
||||||
inset-inline-start: 0;
|
|
||||||
inset-block-start: -0.12rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@supports (scrollbar-color: auto) {
|
|
||||||
html {
|
|
||||||
scrollbar-color: #707070 #343840;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@supports selector(::-webkit-scrollbar) {
|
|
||||||
html::-webkit-scrollbar {
|
|
||||||
width: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
html::-webkit-scrollbar-track {
|
|
||||||
background: #343840;
|
|
||||||
}
|
|
||||||
|
|
||||||
html::-webkit-scrollbar-thumb {
|
|
||||||
background: #707070;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
html::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #b5b5b5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
function refresh(event) {
|
|
||||||
var button = event.currentTarget;
|
|
||||||
button.disabled = true;
|
|
||||||
|
|
||||||
let request = new XMLHttpRequest();
|
|
||||||
request.onload = function () {
|
|
||||||
if (request.status === 200) {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
request.open('GET', `${window.location.origin}/refresh`);
|
|
||||||
request.send();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="flex justify-center items-center min-h-screen bg-{{ theme }}-50 dark:bg-{{ theme }}-950">
|
|
||||||
<div class="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full">
|
|
||||||
<div class="max-w-[48rem] mx-auto h-full my-8">
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<h1 class="text-5xl lg:text-6xl font-bold dark:text-white">Cup</h1>
|
|
||||||
<svg
|
|
||||||
version="1.1"
|
|
||||||
id="Layer_2"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
x="0px"
|
|
||||||
y="0px"
|
|
||||||
viewBox="0 0 128 128"
|
|
||||||
style="enable-background:new 0 0 128 128;"
|
|
||||||
xml:space="preserve"
|
|
||||||
class="size-16"
|
|
||||||
>
|
|
||||||
<path style="fill:#A6CFD6;" d="M65.12,17.55c-17.6-0.53-34.75,5.6-34.83,14.36c-0.04,5.2,1.37,18.6,3.62,48.68s2.25,33.58,3.5,34.95
|
|
||||||
c1.25,1.37,10.02,8.8,25.75,8.8s25.93-6.43,26.93-8.05c0.48-0.78,1.83-17.89,3.5-37.07c1.81-20.84,3.91-43.9,3.99-45.06
|
|
||||||
C97.82,30.66,94.2,18.43,65.12,17.55z"/>
|
|
||||||
<path style="fill:#DCEDF6;" d="M41.4,45.29c-0.12,0.62,1.23,24.16,2.32,27.94c1.99,6.92,9.29,7.38,10.23,4.16
|
|
||||||
c0.9-3.07-0.38-29.29-0.38-29.29s-3.66-0.3-6.43-0.84C44,46.63,41.4,45.29,41.4,45.29z"/>
|
|
||||||
<path style="fill:#6CA4AE;" d="M33.74,32.61c-0.26,8.83,20.02,12.28,30.19,12.22c13.56-0.09,29.48-4.29,29.8-11.7
|
|
||||||
S79.53,21.1,63.35,21.1C49.6,21.1,33.96,25.19,33.74,32.61z"/>
|
|
||||||
<path style="fill:#DC0D27;" d="M84.85,13.1c-0.58,0.64-9.67,30.75-9.67,30.75s2.01-0.33,4-0.79c2.63-0.61,3.76-1.06,3.76-1.06
|
|
||||||
s7.19-22.19,7.64-23.09c0.45-0.9,21.61-7.61,22.31-7.93c0.7-0.32,1.39-0.4,1.46-0.78c0.06-0.38-2.34-6.73-3.11-6.73
|
|
||||||
C110.47,3.47,86.08,11.74,84.85,13.1z"/>
|
|
||||||
<path style="fill:#8A1F0F;" d="M110.55,7.79c1.04,2.73,2.8,3.09,3.55,2.77c0.45-0.19,1.25-1.84,0.01-4.47
|
|
||||||
c-0.99-2.09-2.17-2.74-2.93-2.61C110.42,3.6,109.69,5.53,110.55,7.79z"/>
|
|
||||||
<g>
|
|
||||||
<path style="fill:#8A1F0F;" d="M91.94,18.34c-0.22,0-0.44-0.11-0.58-0.3l-3.99-5.77c-0.22-0.32-0.14-0.75,0.18-0.97
|
|
||||||
c0.32-0.22,0.76-0.14,0.97,0.18l3.99,5.77c0.22,0.32,0.14,0.75-0.18,0.97C92.21,18.3,92.07,18.34,91.94,18.34z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path style="fill:#8A1F0F;" d="M90.28,19.43c-0.18,0-0.35-0.07-0.49-0.2l-5.26-5.12c-0.28-0.27-0.28-0.71-0.01-0.99
|
|
||||||
c0.27-0.28,0.71-0.28,0.99-0.01l5.26,5.12c0.28,0.27,0.28,0.71,0.01,0.99C90.64,19.36,90.46,19.43,90.28,19.43z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path style="fill:#8A1F0F;" d="M89.35,21.22c-0.12,0-0.25-0.03-0.36-0.1l-5.6-3.39c-0.33-0.2-0.44-0.63-0.24-0.96
|
|
||||||
c0.2-0.33,0.63-0.44,0.96-0.24l5.6,3.39c0.33,0.2,0.44,0.63,0.24,0.96C89.82,21.1,89.59,21.22,89.35,21.22z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="shadow-sm bg-white dark:bg-{{ theme }}-900 rounded-md my-8">
|
|
||||||
<dl class="lg:grid-cols-4 grid-cols-2 gap-1 grid overflow-hidden *:relative">
|
|
||||||
{% for metric in metrics %}
|
|
||||||
<div class="gi">
|
|
||||||
<div class="xl:px-8 px-6 py-4 gap-y-2 gap-x-4 justify-between align-baseline flex flex-col h-full">
|
|
||||||
<dt class="text-{{ theme }}-500 dark:text-{{ theme }}-400 leading-6 font-medium">
|
|
||||||
{{ metric.name }}
|
|
||||||
</dt>
|
|
||||||
<div class="flex gap-1 justify-between items-center">
|
|
||||||
<dd class="text-black dark:text-white tracking-tight leading-10 font-medium text-3xl w-full">
|
|
||||||
{{ metric.value }}
|
|
||||||
</dd>
|
|
||||||
{% if metric.name == 'Monitored images' %}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="size-6 text-black dark:text-white shrink-0"
|
|
||||||
>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 4c4.29 0 7.863 2.429 10.665 7.154l.22 .379l.045 .1l.03 .083l.014 .055l.014 .082l.011 .1v.11l-.014 .111a.992 .992 0 0 1 -.026 .11l-.039 .108l-.036 .075l-.016 .03c-2.764 4.836 -6.3 7.38 -10.555 7.499l-.313 .004c-4.396 0 -8.037 -2.549 -10.868 -7.504a1 1 0 0 1 0 -.992c2.831 -4.955 6.472 -7.504 10.868 -7.504zm0 5a3 3 0 1 0 0 6a3 3 0 0 0 0 -6z" />
|
|
||||||
</svg>
|
|
||||||
{% elsif metric.name == 'Up to date' %}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="size-6 text-green-500 shrink-0"
|
|
||||||
>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" />
|
|
||||||
</svg>
|
|
||||||
{% elsif metric.name == 'Updates available' %}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="size-6 text-blue-500 shrink-0"
|
|
||||||
>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z" />
|
|
||||||
</svg>
|
|
||||||
{% elsif metric.name == 'Unknown' %}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="size-6 text-{{ theme }}-500 shrink-0"
|
|
||||||
>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z" />
|
|
||||||
</svg>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
<div class="shadow-sm bg-white dark:bg-{{ theme }}-900 rounded-md my-8">
|
|
||||||
<div class="flex justify-between items-center px-6 py-4 text-{{ theme }}-500">
|
|
||||||
<h3>Last checked: {{ last_updated }}</h3>
|
|
||||||
<button class="group" onclick="refresh(event)">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="group-disabled:animate-spin"
|
|
||||||
>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4,11A8.1,8.1 0 0 1 19.5,9M20,5v4h-4" /><path d="M20,13A8.1,8.1 0 0 1 4.5,15M4,19v-4h4" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ul class="*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-{{ theme }}-800 divide-y dark:text-white">
|
|
||||||
{% for image in images %}
|
|
||||||
<li>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M21 16.008v-8.018a1.98 1.98 0 0 0 -1 -1.717l-7 -4.008a2.016 2.016 0 0 0 -2 0l-7 4.008c-.619 .355 -1 1.01 -1 1.718v8.018c0 .709 .381 1.363 1 1.717l7 4.008a2.016 2.016 0 0 0 2 0l7 -4.008c.619 -.355 1 -1.01 1 -1.718z" />
|
|
||||||
<path d="M12 22v-10" />
|
|
||||||
<path d="M12 12l8.73 -5.04" />
|
|
||||||
<path d="M3.27 6.96l8.73 5.04" />
|
|
||||||
</svg>
|
|
||||||
{{ image.name }}
|
|
||||||
{% if image.has_update == 'false' %}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="text-green-500 ml-auto"
|
|
||||||
>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" />
|
|
||||||
</svg>
|
|
||||||
{% elsif image.has_update == 'true' %}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="text-blue-500 ml-auto"
|
|
||||||
>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z" />
|
|
||||||
</svg>
|
|
||||||
{% elsif image.has_update == 'null' %}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="text-{{ theme }}-500 ml-auto"
|
|
||||||
>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z" />
|
|
||||||
</svg>
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
"src/static/template.liquid"
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
safelist: [
|
|
||||||
{
|
|
||||||
pattern: /(bg|text|divide)-(gray|neutral)-.+/,
|
|
||||||
variants: ["dark"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
24
web/.gitignore
vendored
Normal file
24
web/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
1
web/.tool-versions
Normal file
1
web/.tool-versions
Normal file
@@ -0,0 +1 @@
|
|||||||
|
nodejs 21.6.2
|
||||||
9
web/README.md
Normal file
9
web/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Cup web frontend
|
||||||
|
|
||||||
|
This is the Cup web frontend, built with Vite and React. Once it's built, Cup modifies a few things (notably the theme) and sends the result to the client.
|
||||||
|
|
||||||
|
# Development
|
||||||
|
|
||||||
|
Requirements: Bun, Node.js 20+
|
||||||
|
|
||||||
|
Install dependencies with `bun install` and start the development server with `bun dev`.
|
||||||
BIN
web/bun.lockb
Executable file
BIN
web/bun.lockb
Executable file
Binary file not shown.
36
web/eslint.config.js
Normal file
36
web/eslint.config.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ["dist"] },
|
||||||
|
{
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
],
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react-refresh/only-export-components": [
|
||||||
|
"warn",
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
16
web/index.html
Normal file
16
web/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf8" />
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
|
<title>Cup</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
37
web/package.json
Normal file
37
web/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"fmt": "prettier --write ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tabler/icons-react": "^3.14.0",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.9.0",
|
||||||
|
"@types/node": "^22.5.1",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^9.9.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.9",
|
||||||
|
"globals": "^15.9.0",
|
||||||
|
"postcss": "^8.4.42",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"tailwindcss": "^3.4.10",
|
||||||
|
"typescript": "^5.5.3",
|
||||||
|
"typescript-eslint": "^8.0.1",
|
||||||
|
"vite": "^5.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
web/postcss.config.js
Normal file
6
web/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
web/public/apple-touch-icon.png
Normal file
BIN
web/public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
web/public/favicon.ico
Normal file
BIN
web/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
29
web/public/favicon.svg
Normal file
29
web/public/favicon.svg
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
|
||||||
|
<path style="fill:#A6CFD6;" d="M65.12,17.55c-17.6-0.53-34.75,5.6-34.83,14.36c-0.04,5.2,1.37,18.6,3.62,48.68s2.25,33.58,3.5,34.95
|
||||||
|
c1.25,1.37,10.02,8.8,25.75,8.8s25.93-6.43,26.93-8.05c0.48-0.78,1.83-17.89,3.5-37.07c1.81-20.84,3.91-43.9,3.99-45.06
|
||||||
|
C97.82,30.66,94.2,18.43,65.12,17.55z"/>
|
||||||
|
<path style="fill:#DCEDF6;" d="M41.4,45.29c-0.12,0.62,1.23,24.16,2.32,27.94c1.99,6.92,9.29,7.38,10.23,4.16
|
||||||
|
c0.9-3.07-0.38-29.29-0.38-29.29s-3.66-0.3-6.43-0.84C44,46.63,41.4,45.29,41.4,45.29z"/>
|
||||||
|
<path style="fill:#6CA4AE;" d="M33.74,32.61c-0.26,8.83,20.02,12.28,30.19,12.22c13.56-0.09,29.48-4.29,29.8-11.7
|
||||||
|
S79.53,21.1,63.35,21.1C49.6,21.1,33.96,25.19,33.74,32.61z"/>
|
||||||
|
<path style="fill:#DC0D27;" d="M84.85,13.1c-0.58,0.64-9.67,30.75-9.67,30.75s2.01-0.33,4-0.79c2.63-0.61,3.76-1.06,3.76-1.06
|
||||||
|
s7.19-22.19,7.64-23.09c0.45-0.9,21.61-7.61,22.31-7.93c0.7-0.32,1.39-0.4,1.46-0.78c0.06-0.38-2.34-6.73-3.11-6.73
|
||||||
|
C110.47,3.47,86.08,11.74,84.85,13.1z"/>
|
||||||
|
<path style="fill:#8A1F0F;" d="M110.55,7.79c1.04,2.73,2.8,3.09,3.55,2.77c0.45-0.19,1.25-1.84,0.01-4.47
|
||||||
|
c-0.99-2.09-2.17-2.74-2.93-2.61C110.42,3.6,109.69,5.53,110.55,7.79z"/>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#8A1F0F;" d="M91.94,18.34c-0.22,0-0.44-0.11-0.58-0.3l-3.99-5.77c-0.22-0.32-0.14-0.75,0.18-0.97
|
||||||
|
c0.32-0.22,0.76-0.14,0.97,0.18l3.99,5.77c0.22,0.32,0.14,0.75-0.18,0.97C92.21,18.3,92.07,18.34,91.94,18.34z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#8A1F0F;" d="M90.28,19.43c-0.18,0-0.35-0.07-0.49-0.2l-5.26-5.12c-0.28-0.27-0.28-0.71-0.01-0.99
|
||||||
|
c0.27-0.28,0.71-0.28,0.99-0.01l5.26,5.12c0.28,0.27,0.28,0.71,0.01,0.99C90.64,19.36,90.46,19.43,90.28,19.43z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#8A1F0F;" d="M89.35,21.22c-0.12,0-0.25-0.03-0.36-0.1l-5.6-3.39c-0.33-0.2-0.44-0.63-0.24-0.96
|
||||||
|
c0.2-0.33,0.63-0.44,0.96-0.24l5.6,3.39c0.33,0.2,0.44,0.63,0.24,0.96C89.82,21.1,89.59,21.22,89.35,21.22z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
78
web/src/App.tsx
Normal file
78
web/src/App.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { MouseEvent, useState } from "react";
|
||||||
|
import Logo from "./components/Logo";
|
||||||
|
import Statistic from "./components/Statistic";
|
||||||
|
import Image from "./components/Image";
|
||||||
|
import { IconRefresh } from "@tabler/icons-react";
|
||||||
|
import { LastChecked } from "./components/LastChecked";
|
||||||
|
import Loading from "./components/Loading";
|
||||||
|
import { Data } from "./types";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [data, setData] = useState<Data | null>(null);
|
||||||
|
const theme = "neutral"; // Stupid, I know but I want both the dev and prod to work easily.
|
||||||
|
if (!data) return <Loading onLoad={setData} />;
|
||||||
|
const refresh = (event: MouseEvent) => {
|
||||||
|
const btn = event.currentTarget as HTMLButtonElement;
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
let request = new XMLHttpRequest();
|
||||||
|
request.onload = function () {
|
||||||
|
if (request.status === 200) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
request.open(
|
||||||
|
"GET",
|
||||||
|
process.env.NODE_ENV === "production"
|
||||||
|
? "/json"
|
||||||
|
: `http://${window.location.hostname}:8000/json`,
|
||||||
|
);
|
||||||
|
request.send();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex justify-center min-h-screen bg-${theme}-50 dark:bg-${theme}-950`}
|
||||||
|
>
|
||||||
|
<div className="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full">
|
||||||
|
<div className="flex flex-col max-w-[48rem] mx-auto h-full my-8">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<h1 className="text-5xl lg:text-6xl font-bold dark:text-white">
|
||||||
|
Cup
|
||||||
|
</h1>
|
||||||
|
<Logo />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`shadow-sm bg-white dark:bg-${theme}-900 rounded-md my-8`}
|
||||||
|
>
|
||||||
|
<dl className="lg:grid-cols-4 md:grid-cols-2 gap-1 grid-cols-1 grid overflow-hidden *:relative">
|
||||||
|
{Object.entries(data.metrics).map(([name, value]) => (
|
||||||
|
<Statistic name={name} value={value} key={name} />
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`shadow-sm bg-white dark:bg-${theme}-900 rounded-md my-8`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex justify-between items-center px-6 py-4 text-${theme}-500`}
|
||||||
|
>
|
||||||
|
<LastChecked datetime={data.last_updated} />
|
||||||
|
<button className="group" onClick={refresh}>
|
||||||
|
<IconRefresh className="-scale-x-100 group-disabled:animate-spin" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
className={`*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-${theme}-800 divide-y dark:text-white`}
|
||||||
|
>
|
||||||
|
{Object.entries(data.images).map(([name, status]) => (
|
||||||
|
<Image name={name} status={status} key={name} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
30
web/src/components/Image.tsx
Normal file
30
web/src/components/Image.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
IconCircleArrowUpFilled,
|
||||||
|
IconCircleCheckFilled,
|
||||||
|
IconCube,
|
||||||
|
IconHelpCircleFilled,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export default function Image({
|
||||||
|
name,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
status: boolean | null;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<li className="break-all">
|
||||||
|
<IconCube className="size-6 shrink-0" />
|
||||||
|
{name}
|
||||||
|
{status == false && (
|
||||||
|
<IconCircleCheckFilled className="text-green-500 ml-auto size-6 shrink-0" />
|
||||||
|
)}
|
||||||
|
{status == true && (
|
||||||
|
<IconCircleArrowUpFilled className="text-blue-500 ml-auto size-6 shrink-0" />
|
||||||
|
)}
|
||||||
|
{status == null && (
|
||||||
|
<IconHelpCircleFilled className="text-gray-500 ml-auto size-6 shrink-0" />
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
web/src/components/LastChecked.tsx
Normal file
6
web/src/components/LastChecked.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { intlFormatDistance } from "date-fns/intlFormatDistance";
|
||||||
|
|
||||||
|
export function LastChecked({ datetime }: { datetime: string }) {
|
||||||
|
const date = intlFormatDistance(new Date(datetime), new Date());
|
||||||
|
return <h3>Last checked: {date}</h3>;
|
||||||
|
}
|
||||||
34
web/src/components/Loading.tsx
Normal file
34
web/src/components/Loading.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { IconLoader2 } from "@tabler/icons-react";
|
||||||
|
import { Data } from "../types";
|
||||||
|
import Logo from "./Logo";
|
||||||
|
|
||||||
|
export default function Loading({ onLoad }: { onLoad: (data: Data) => void }) {
|
||||||
|
const theme = "neutral";
|
||||||
|
fetch(
|
||||||
|
process.env.NODE_ENV === "production"
|
||||||
|
? "/json"
|
||||||
|
: `http://${window.location.hostname}:8000/json`,
|
||||||
|
).then((response) => response.json().then((data) => onLoad(data)));
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex justify-center min-h-screen bg-${theme}-50 dark:bg-${theme}-950`}
|
||||||
|
>
|
||||||
|
<div className="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full absolute overflow-hidden">
|
||||||
|
<div className="flex flex-col max-w-[48rem] mx-auto h-full my-8">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<h1 className="text-5xl lg:text-6xl font-bold dark:text-white">
|
||||||
|
Cup
|
||||||
|
</h1>
|
||||||
|
<Logo />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`h-full flex justify-center
|
||||||
|
items-center gap-1 text-${theme}-500 dark:text-${theme}-400`}
|
||||||
|
>
|
||||||
|
Loading <IconLoader2 className="animate-spin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
web/src/components/Logo.tsx
Normal file
54
web/src/components/Logo.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
export default function Logo() {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 128 128" className="size-14 lg:size-16">
|
||||||
|
<path
|
||||||
|
style={{ fill: "#A6CFD6" }}
|
||||||
|
d="M65.12,17.55c-17.6-0.53-34.75,5.6-34.83,14.36c-0.04,5.2,1.37,18.6,3.62,48.68s2.25,33.58,3.5,34.95
|
||||||
|
c1.25,1.37,10.02,8.8,25.75,8.8s25.93-6.43,26.93-8.05c0.48-0.78,1.83-17.89,3.5-37.07c1.81-20.84,3.91-43.9,3.99-45.06
|
||||||
|
C97.82,30.66,94.2,18.43,65.12,17.55z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
style={{ fill: "#DCEDF6" }}
|
||||||
|
d="M41.4,45.29c-0.12,0.62,1.23,24.16,2.32,27.94c1.99,6.92,9.29,7.38,10.23,4.16
|
||||||
|
c0.9-3.07-0.38-29.29-0.38-29.29s-3.66-0.3-6.43-0.84C44,46.63,41.4,45.29,41.4,45.29z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
style={{ fill: "#6CA4AE" }}
|
||||||
|
d="M33.74,32.61c-0.26,8.83,20.02,12.28,30.19,12.22c13.56-0.09,29.48-4.29,29.8-11.7
|
||||||
|
S79.53,21.1,63.35,21.1C49.6,21.1,33.96,25.19,33.74,32.61z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
style={{ fill: "#DC0D27" }}
|
||||||
|
d="M84.85,13.1c-0.58,0.64-9.67,30.75-9.67,30.75s2.01-0.33,4-0.79c2.63-0.61,3.76-1.06,3.76-1.06
|
||||||
|
s7.19-22.19,7.64-23.09c0.45-0.9,21.61-7.61,22.31-7.93c0.7-0.32,1.39-0.4,1.46-0.78c0.06-0.38-2.34-6.73-3.11-6.73
|
||||||
|
C110.47,3.47,86.08,11.74,84.85,13.1z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
style={{ fill: "#8A1F0F" }}
|
||||||
|
d="M110.55,7.79c1.04,2.73,2.8,3.09,3.55,2.77c0.45-0.19,1.25-1.84,0.01-4.47
|
||||||
|
c-0.99-2.09-2.17-2.74-2.93-2.61C110.42,3.6,109.69,5.53,110.55,7.79z"
|
||||||
|
/>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
style={{ fill: "#8A1F0F" }}
|
||||||
|
d="M91.94,18.34c-0.22,0-0.44-0.11-0.58-0.3l-3.99-5.77c-0.22-0.32-0.14-0.75,0.18-0.97
|
||||||
|
c0.32-0.22,0.76-0.14,0.97,0.18l3.99,5.77c0.22,0.32,0.14,0.75-0.18,0.97C92.21,18.3,92.07,18.34,91.94,18.34z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
style={{ fill: "#8A1F0F" }}
|
||||||
|
d="M90.28,19.43c-0.18,0-0.35-0.07-0.49-0.2l-5.26-5.12c-0.28-0.27-0.28-0.71-0.01-0.99
|
||||||
|
c0.27-0.28,0.71-0.28,0.99-0.01l5.26,5.12c0.28,0.27,0.28,0.71,0.01,0.99C90.64,19.36,90.46,19.43,90.28,19.43z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
style={{ fill: "#8A1F0F" }}
|
||||||
|
d="M89.35,21.22c-0.12,0-0.25-0.03-0.36-0.1l-5.6-3.39c-0.33-0.2-0.44-0.63-0.24-0.96
|
||||||
|
c0.2-0.33,0.63-0.44,0.96-0.24l5.6,3.39c0.33,0.2,0.44,0.63,0.24,0.96C89.82,21.1,89.59,21.22,89.35,21.22z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
web/src/components/Statistic.tsx
Normal file
48
web/src/components/Statistic.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
IconCircleArrowUpFilled,
|
||||||
|
IconCircleCheckFilled,
|
||||||
|
IconEyeFilled,
|
||||||
|
IconHelpCircleFilled,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export default function Statistic({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
}) {
|
||||||
|
const theme = "neutral";
|
||||||
|
name = name.replaceAll("_", " ");
|
||||||
|
name = name.slice(0, 1).toUpperCase() + name.slice(1); // Capitalize name
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`before:bg-${theme}-200 before:dark:bg-${theme}-800 after:bg-${theme}-200 after:dark:bg-${theme}-800 gi`}
|
||||||
|
>
|
||||||
|
<div className="xl:px-8 px-6 py-4 gap-y-2 gap-x-4 justify-between align-baseline flex flex-col h-full">
|
||||||
|
<dt
|
||||||
|
className={`text-${theme}-500 dark:text-${theme}-400 leading-6 font-medium`}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</dt>
|
||||||
|
<div className="flex gap-1 justify-between items-center">
|
||||||
|
<dd className="text-black dark:text-white tracking-tight leading-10 font-medium text-3xl w-full">
|
||||||
|
{value}
|
||||||
|
</dd>
|
||||||
|
{name == "Monitored images" && (
|
||||||
|
<IconEyeFilled className="size-6 text-black dark:text-white shrink-0" />
|
||||||
|
)}
|
||||||
|
{name == "Up to date" && (
|
||||||
|
<IconCircleCheckFilled className="size-6 text-green-500 shrink-0" />
|
||||||
|
)}
|
||||||
|
{name == "Update available" && (
|
||||||
|
<IconCircleArrowUpFilled className="size-6 text-blue-500 shrink-0" />
|
||||||
|
)}
|
||||||
|
{name == "Unknown" && (
|
||||||
|
<IconHelpCircleFilled className="size-6 text-gray-500 shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
web/src/index.css
Normal file
54
web/src/index.css
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Kinda hacky, but thank you https://geary.co/internal-borders-css-grid/ */
|
||||||
|
.gi {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gi::before,
|
||||||
|
.gi::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gi::before {
|
||||||
|
inline-size: 1px;
|
||||||
|
block-size: 100vh;
|
||||||
|
inset-inline-start: -0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gi::after {
|
||||||
|
inline-size: 100vw;
|
||||||
|
block-size: 1px;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
inset-block-start: -0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (scrollbar-color: auto) {
|
||||||
|
html {
|
||||||
|
scrollbar-color: #707070 #343840;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports selector(::-webkit-scrollbar) {
|
||||||
|
html::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar-track {
|
||||||
|
background: #343840;
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar-thumb {
|
||||||
|
background: #707070;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #b5b5b5;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
web/src/main.tsx
Normal file
10
web/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import App from "./App.tsx";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
12
web/src/types.ts
Normal file
12
web/src/types.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export type Data = {
|
||||||
|
metrics: {
|
||||||
|
monitored_images: number;
|
||||||
|
up_to_date: number;
|
||||||
|
update_available: number;
|
||||||
|
unknown: number;
|
||||||
|
};
|
||||||
|
images: {
|
||||||
|
[key: string]: boolean | null;
|
||||||
|
};
|
||||||
|
last_updated: string;
|
||||||
|
};
|
||||||
1
web/src/vite-env.d.ts
vendored
Normal file
1
web/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
37
web/tailwind.config.js
Normal file
37
web/tailwind.config.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./src/App.tsx", "./src/components/*.tsx"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
safelist: [
|
||||||
|
// Generate minimum extra CSS
|
||||||
|
{
|
||||||
|
pattern: /bg-(gray|neutral)-50/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /bg-(gray|neutral)-(900|950)/,
|
||||||
|
variants: ["dark"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /bg-(gray|neutral)-200/,
|
||||||
|
variants: ["before", "after"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /bg-(gray|neutral)-800/,
|
||||||
|
variants: ["before:dark", "after:dark"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /text-(gray|neutral)-400/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /text-(gray|neutral)-500/,
|
||||||
|
variants: ["dark"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /divide-(gray|neutral)-800/,
|
||||||
|
variants: ["dark"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
24
web/tsconfig.app.json
Normal file
24
web/tsconfig.app.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
web/tsconfig.json
Normal file
7
web/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
22
web/tsconfig.node.json
Normal file
22
web/tsconfig.node.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
16
web/vite.config.ts
Normal file
16
web/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react-swc";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
rollupOptions: { // https://stackoverflow.com/q/69614671/vite-without-hash-in-filename#75344943
|
||||||
|
output: {
|
||||||
|
entryFileNames: `assets/[name].js`,
|
||||||
|
chunkFileNames: `assets/[name].js`,
|
||||||
|
assetFileNames: `assets/[name].[ext]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user