diff --git a/.gitignore b/.gitignore index dd8c74234a53e673242f35f1a09f6b50c2254cb9..c1c26692c4c755ced526686663daf912b5ea4f4a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ bin # build output dist/ -assets/ .astro/ # dependencies diff --git a/Cargo.lock b/Cargo.lock index dfb1c2887ade847bbd8b8a26ea998dd5173c2a1b..6cf7f4b1723134b1c8010f29185f5f7cc7a3ed83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1570,6 +1570,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "os_info" +version = "3.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" +dependencies = [ + "log", + "serde", + "windows-sys 0.52.0", +] + [[package]] name = "overload" version = "0.1.1" @@ -1780,6 +1791,7 @@ dependencies = [ "merkle_hash", "nix 0.27.1", "once_cell", + "os_info", "pretty_env_logger", "prometheus", "psutil", diff --git a/Cargo.toml b/Cargo.toml index 5614e740185aabcc3c199bc002fc207e46d98d71..9bc73af536617702348cd618d585314d3751d8da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ chrono = { version = "0.4.31", features = ["serde"] } serde = { version = "1.0.193", features = ["derive"] } nix = { version = "0.27.1", features = ["process", "signal"] } utoipa = { version = "4.1.0", features = ["serde_yaml", "non_strict_integers"] } +os_info = "3.8.2" [dependencies.reqwest] version = "0.11.23" diff --git a/src/cli/mod.rs b/src/cli/mod.rs index facc74abfab8cd9900689121b5efc226e786c4d6..00fd9124841f54f7fe642bf05df77dec85b804ca 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -122,6 +122,8 @@ pub fn remove(item: &Item, server_name: &String) { None => crashln!("{} Process ({name}) not found", *helpers::FAIL), }, } + + super::daemon::reset(); } pub fn info(item: &Item, format: &String, server_name: &String) { diff --git a/src/daemon/api/mod.rs b/src/daemon/api/mod.rs index a35eb2df68c1ff4eadf0636dc55a4cf7df286f53..9f4935cba58dfb99d33b712d1187cbf2970b4b62 100644 --- a/src/daemon/api/mod.rs +++ b/src/daemon/api/mod.rs @@ -61,6 +61,7 @@ lazy_static! { routes::logs_handler, routes::remote_list, routes::remote_info, + routes::remote_metrics, routes::remote_logs, routes::remote_rename, routes::remote_action, @@ -199,6 +200,7 @@ pub async fn start(webui: bool) { routes::logs_handler, routes::logs_raw_handler, routes::metrics_handler, + routes::remote_metrics, routes::stream_info, routes::stream_metrics, routes::prometheus_handler, diff --git a/src/daemon/api/routes.rs b/src/daemon/api/routes.rs index fe741410ea7d6d62b3887c8693ff8dfe2411a516..b284a44d31a50b829f927c18489963b7214cf4ac 100644 --- a/src/daemon/api/routes.rs +++ b/src/daemon/api/routes.rs @@ -6,7 +6,6 @@ use macros_rs::{fmtstr, string, ternary, then}; use prometheus::{Encoder, TextEncoder}; use psutil::process::{MemoryInfo, Process}; use reqwest::header::HeaderValue; -use serde::Deserialize; use tera::Context; use utoipa::ToSchema; @@ -15,7 +14,7 @@ use rocket::{ http::{ContentType, Status}, post, response::stream::{Event, EventStream}, - serde::{json::Json, Serialize}, + serde::{json::Json, Deserialize, Serialize}, State, }; @@ -107,24 +106,32 @@ pub(crate) struct LogResponse { logs: Vec, } -#[derive(Serialize, ToSchema)] +#[derive(Serialize, Deserialize, ToSchema)] pub struct MetricsRoot { + pub raw: Raw, pub version: Version, + pub os: crate::globals::Os, pub daemon: Daemon, } -#[derive(Serialize, ToSchema)] +#[derive(Serialize, Deserialize, ToSchema)] +pub struct Raw { + pub memory_usage: Option, + pub cpu_percent: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] pub struct Version { #[schema(example = "v1.0.0")] pub pkg: String, - pub hash: Option<&'static str>, + pub hash: Option, #[schema(example = "2000-01-01")] - pub build_date: &'static str, + pub build_date: String, #[schema(example = "release")] - pub target: &'static str, + pub target: String, } -#[derive(Serialize, ToSchema)] +#[derive(Serialize, Deserialize, ToSchema)] pub struct Daemon { pub pid: Option, #[schema(example = true)] @@ -136,7 +143,7 @@ pub struct Daemon { pub stats: Stats, } -#[derive(Serialize, ToSchema)] +#[derive(Serialize, Deserialize, ToSchema)] pub struct Stats { pub memory_usage: String, pub cpu_percent: String, @@ -172,10 +179,10 @@ pub async fn view_process(id: usize, state: &State, _webui: EnableWeb Ok((ContentType::HTML, render("view", &state, &mut ctx)?)) } -#[get("/status/")] -pub async fn server_status(id: usize, state: &State, _webui: EnableWebUI) -> Result<(ContentType, String), NotFound> { +#[get("/status/")] +pub async fn server_status(name: String, state: &State, _webui: EnableWebUI) -> Result<(ContentType, String), NotFound> { let mut ctx = Context::new(); - ctx.insert("process_id", &id); + ctx.insert("server_name", &name); Ok((ContentType::HTML, render("status", &state, &mut ctx)?)) } @@ -783,6 +790,8 @@ pub async fn action_handler(id: usize, body: Json, _t: Token) -> Res pub async fn get_metrics() -> MetricsRoot { let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["metrics"]).start_timer(); + let os_info = crate::globals::get_os_info(); + let mut pid: Option = None; let mut cpu_percent: Option = None; let mut uptime: Option> = None; @@ -801,36 +810,41 @@ pub async fn get_metrics() -> MetricsRoot { } } - let memory_usage = match memory_usage { + let memory_usage_fmt = match &memory_usage { Some(usage) => helpers::format_memory(usage.rss()), None => string!("0b"), }; - let cpu_percent = match cpu_percent { + let cpu_percent_fmt = match cpu_percent { Some(percent) => format!("{:.2}%", percent), None => string!("0%"), }; - let uptime = match uptime { + let uptime_fmt = match uptime { Some(uptime) => helpers::format_duration(uptime), None => string!("none"), }; timer.observe_duration(); MetricsRoot { + os: os_info.clone(), + raw: Raw { memory_usage, cpu_percent }, version: Version { - target: env!("PROFILE"), - build_date: env!("BUILD_DATE"), + target: env!("PROFILE").into(), + build_date: env!("BUILD_DATE").into(), pkg: format!("v{}", env!("CARGO_PKG_VERSION")), - hash: ternary!(env!("GIT_HASH_FULL") == "", None, Some(env!("GIT_HASH_FULL"))), + hash: ternary!(env!("GIT_HASH_FULL") == "", None, Some(env!("GIT_HASH_FULL").into())), }, daemon: Daemon { pid, - uptime, + uptime: uptime_fmt, running: pid::exists(), process_count: runner.count(), daemon_type: global!("pmc.daemon.kind"), - stats: Stats { memory_usage, cpu_percent }, + stats: Stats { + memory_usage: memory_usage_fmt, + cpu_percent: cpu_percent_fmt, + }, }, } } @@ -847,6 +861,45 @@ pub async fn get_metrics() -> MetricsRoot { )] pub async fn metrics_handler(_t: Token) -> Json { Json(get_metrics().await) } +#[get("/remote//metrics")] +#[utoipa::path(get, tag = "Remote", path = "/remote/{name}/metrics", security((), ("api_key" = [])), + params(("name" = String, Path, description = "Name of remote daemon", example = "example")), + responses( + (status = 200, description = "Get remote metrics", body = MetricsRoot), + ( + status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage, + example = json!({"code": 401, "message": "Unauthorized"}) + ) + ) +)] +pub async fn remote_metrics(name: String, _t: Token) -> Result, GenericError> { + let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["info"]).start_timer(); + + if let Some(servers) = config::servers().servers { + let (address, (client, headers)) = match servers.get(&name) { + Some(server) => (&server.address, client(&server.token).await), + None => return Err(generic_error(Status::NotFound, string!("Server was not found"))), + }; + + HTTP_COUNTER.inc(); + timer.observe_duration(); + + match client.get(fmtstr!("{address}/daemon/metrics")).headers(headers).send().await { + Ok(data) => { + if data.status() != 200 { + let err = data.json::().await.unwrap(); + Err(generic_error(err.code, err.message)) + } else { + Ok(Json(data.json::().await.unwrap())) + } + } + Err(err) => Err(generic_error(Status::InternalServerError, err.to_string())), + } + } else { + Err(generic_error(Status::BadRequest, string!("No servers have been added"))) + } +} + #[get("/live/daemon//metrics")] pub async fn stream_metrics(server: String, _t: Token) -> EventStream![] { EventStream! { @@ -857,7 +910,7 @@ pub async fn stream_metrics(server: String, _t: Token) -> EventStream![] { None => loop { let response = get_metrics().await; yield Event::data(serde_json::to_string(&response).unwrap()); - sleep(Duration::from_millis(1500)) + sleep(Duration::from_millis(500)) }, }; @@ -889,7 +942,7 @@ pub async fn stream_info(server: String, id: usize, _t: Token) -> EventStream![] None => loop { let item = runner.refresh().get(id); yield Event::data(serde_json::to_string(&item.fetch()).unwrap()); - sleep(Duration::from_millis(1500)) + sleep(Duration::from_millis(1000)) }, }; diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index d5ff15f1610478505627645c02d86778b9efb013..a01640ceafb1fefec69c613ec3e75aac07df9079 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -307,7 +307,7 @@ pub fn reset() { None => runner.set_id(Id::new(0)), } - println!("{} PMC Successfully reset (index={})", *helpers::SUCCESS, runner.id); + println!("{} Successfully reset (index={})", *helpers::SUCCESS, runner.id); } pub mod pid; diff --git a/src/daemon/pid.rs b/src/daemon/pid.rs index 305efea2324d0b83ba78c5da6ec0ac1c62a09671..dfc4af415cffeaf1fcbbf616df88d76b82be86eb 100644 --- a/src/daemon/pid.rs +++ b/src/daemon/pid.rs @@ -4,10 +4,10 @@ use core::fmt; use global_placeholders::global; use macros_rs::crashln; use pmc::{file::Exists, helpers}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::{convert::TryFrom, fs, io}; -#[derive(Copy, Clone, Serialize)] +#[derive(Copy, Clone, Serialize, Deserialize)] pub struct Pid(i32); impl Pid { diff --git a/src/globals.rs b/src/globals.rs index 393c0d05d5b3af4447b7288030e81bed0c502435..1d73ac7e2cba26ba5e7480b464f49fe49985af2a 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -1,7 +1,32 @@ use global_placeholders::init; use macros_rs::crashln; +use once_cell::sync::OnceCell; use pmc::{config, file::Exists, helpers}; +use serde::{Deserialize, Serialize}; use std::fs; +use utoipa::ToSchema; + +#[derive(Clone, Serialize, Deserialize, ToSchema)] +pub struct Os { + pub name: os_info::Type, + pub version: String, + pub arch: String, + pub bitness: os_info::Bitness, +} + +pub static OS_INFO: OnceCell = OnceCell::new(); + +pub fn get_os_info() -> &'static Os { + OS_INFO.get_or_init(|| { + let os = os_info::get(); + Os { + name: os.os_type(), + version: os.version().to_string(), + arch: os.architecture().unwrap().into(), + bitness: os.bitness(), + } + }) +} pub(crate) fn init() { match home::home_dir() { diff --git a/src/webui/npm-shrinkwrap.json b/src/webui/npm-shrinkwrap.json index e03612bc1fdfcb1ac26138d63ccf7efc2ed9184c..dcf6ef02589257e0c1e370ccd7b9914aceafae35 100644 --- a/src/webui/npm-shrinkwrap.json +++ b/src/webui/npm-shrinkwrap.json @@ -11,6 +11,7 @@ "dependencies": { "@astrojs/react": "^3.6.0", "@astrojs/tailwind": "^5.1.0", + "@emotion/styled": "^11.11.5", "@headlessui/react": "^2.1.1", "@heroicons/react": "^2.0.18", "@nanostores/persistent": "^0.9.1", @@ -18,11 +19,14 @@ "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", "astro": "^4.11.3", + "chart.js": "^4.4.3", "glob": "^10.3.10", "ky": "^1.1.3", "match-sorter": "^6.3.1", "react": "^18.0.0", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.0.0", + "sse.js": "^2.5.0", "tailwindcss": "^3.0.24", "typescript": "^5.5.2" }, @@ -572,6 +576,174 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==", + "license": "MIT", + "peer": true + }, + "node_modules/@emotion/styled": { + "version": "11.11.5", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz", + "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.2", + "@emotion/serialize": "^1.1.4", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==", + "license": "MIT", + "peer": true + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1553,6 +1725,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", + "license": "MIT" + }, "node_modules/@nanostores/persistent": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@nanostores/persistent/-/persistent-0.9.1.tgz", @@ -2045,6 +2223,12 @@ "@types/unist": "*" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", @@ -2356,6 +2540,21 @@ "dequal": "^2.0.3" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -2489,6 +2688,15 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", @@ -2582,6 +2790,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chart.js": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", + "integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2770,6 +2990,31 @@ "node": ">= 0.6" } }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2795,9 +3040,10 @@ } }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" }, "node_modules/debug": { "version": "4.3.5", @@ -2936,6 +3182,21 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-ex/node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/es-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", @@ -3097,6 +3358,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3501,6 +3768,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", @@ -3529,6 +3806,22 @@ "node": ">=16.17.0" } }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/import-meta-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", @@ -3778,6 +4071,12 @@ "node": ">=4" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5135,6 +5434,36 @@ "node": ">=6" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-latin": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", @@ -5224,6 +5553,15 @@ "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -5549,6 +5887,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -5561,6 +5909,13 @@ "react": "^18.2.0" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT", + "peer": true + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -5757,6 +6112,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -6075,6 +6439,15 @@ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -6099,6 +6472,12 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/sse.js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/sse.js/-/sse.js-2.5.0.tgz", + "integrity": "sha512-I7zYndqOOkNpz9KIdFZ8c8A7zs1YazNewBr8Nsi/tqThfJkVPuP1q7UE2h4B0RwoWZxbBYpd06uoW3NI3SaZXg==", + "license": "Apache-2.0" + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -6242,6 +6621,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", diff --git a/src/webui/package.json b/src/webui/package.json index edbc9916abadd1cd19ac9fdaa20d7ec34f2cad4d..7c75f3bf6b9a02fb6b4079704da09e43999cd838 100644 --- a/src/webui/package.json +++ b/src/webui/package.json @@ -13,6 +13,7 @@ "dependencies": { "@astrojs/react": "^3.6.0", "@astrojs/tailwind": "^5.1.0", + "@emotion/styled": "^11.11.5", "@headlessui/react": "^2.1.1", "@heroicons/react": "^2.0.18", "@nanostores/persistent": "^0.9.1", @@ -20,11 +21,14 @@ "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", "astro": "^4.11.3", + "chart.js": "^4.4.3", "glob": "^10.3.10", "ky": "^1.1.3", "match-sorter": "^6.3.1", "react": "^18.0.0", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.0.0", + "sse.js": "^2.5.0", "tailwindcss": "^3.0.24", "typescript": "^5.5.2" }, diff --git a/src/webui/src/public/banner.png b/src/webui/public/assets/banner.png similarity index 100% rename from src/webui/src/public/banner.png rename to src/webui/public/assets/banner.png diff --git a/src/webui/src/public/favicon.svg b/src/webui/public/assets/favicon.svg similarity index 100% rename from src/webui/src/public/favicon.svg rename to src/webui/public/assets/favicon.svg diff --git a/src/webui/public/assets/login.svg b/src/webui/public/assets/login.svg new file mode 100644 index 0000000000000000000000000000000000000000..844f26400664014d49b73bbd810ed99f17963bc6 --- /dev/null +++ b/src/webui/public/assets/login.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/webui/src/api.ts b/src/webui/src/api.ts index 84c07fb8f10910d36d66584fef546a22da93038d..a192f8027ef75827dbf617a8e16ad4d1ab37db6b 100644 --- a/src/webui/src/api.ts +++ b/src/webui/src/api.ts @@ -1,6 +1,8 @@ import ky from 'ky'; import { $settings } from '@/store'; -export const api = ky.create({ - headers: { token: $settings.get().token } -}); +export { SSE } from 'sse.js'; + +export const headers = { token: $settings.get().token }; + +export const api = ky.create({ headers }); diff --git a/src/webui/src/components/base.astro b/src/webui/src/components/base.astro index 60a722689a5a8ac89d548c84f26f1498f78b9e70..07829a552a8d8e89100c19306a43e641b1cefd54 100644 --- a/src/webui/src/components/base.astro +++ b/src/webui/src/components/base.astro @@ -1,7 +1,5 @@ --- import '@/styles.css' -import banner from '@/public/banner.png?url' -import favicon from '@/public/favicon.svg?url' import { ViewTransitions } from "astro:transitions"; interface Props { @@ -21,20 +19,20 @@ const { title, description } = Astro.props; {title} - + - + - + - + diff --git a/src/webui/src/components/navbar.astro b/src/webui/src/components/navbar.astro index d4666caf7b4b78a9302294629b8ca16ea0cb566d..bfc244d9fc625b0886e6d37927bafe04da6f40fd 100644 --- a/src/webui/src/components/navbar.astro +++ b/src/webui/src/components/navbar.astro @@ -1,19 +1,18 @@ --- const { active } = Astro.props; -import favicon from '@/public/favicon.svg?url' --- -