feat(homelab)!: migrate to svelte kit, add more server stats endpoints
This commit is contained in:
163
nix/homelab/api/Cargo.lock
generated
163
nix/homelab/api/Cargo.lock
generated
@@ -234,6 +234,15 @@ version = "0.2.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-broadcast"
|
name = "async-broadcast"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
@@ -378,8 +387,12 @@ version = "0.4.42"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -461,6 +474,12 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@@ -669,6 +688,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
@@ -691,6 +711,17 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-executor"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -833,7 +864,9 @@ name = "homelab-api"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
|
"chrono",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
"futures",
|
||||||
"k8s-openapi",
|
"k8s-openapi",
|
||||||
"kube",
|
"kube",
|
||||||
"nom",
|
"nom",
|
||||||
@@ -842,6 +875,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -984,6 +1018,30 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.64"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@@ -1217,6 +1275,7 @@ dependencies = [
|
|||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
@@ -2124,9 +2183,21 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2 0.6.1",
|
"socket2 0.6.1",
|
||||||
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-rustls"
|
||||||
version = "0.26.4"
|
version = "0.26.4"
|
||||||
@@ -2137,6 +2208,18 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"tokio",
|
||||||
|
"tungstenite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -2236,6 +2319,23 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http 1.4.0",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"rand",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
@@ -2290,6 +2390,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf-8"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -2371,12 +2477,65 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
@@ -2650,9 +2809,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.12"
|
version = "1.0.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"
|
checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zstd"
|
name = "zstd"
|
||||||
|
|||||||
@@ -12,5 +12,8 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
tokio = "1.49.0"
|
tokio = "1.49.0"
|
||||||
kube = { version = "2.0.1", features = ["client", "runtime"] }
|
tokio-util = { version = "0.7", features = ["io"] }
|
||||||
|
kube = { version = "2.0.1", features = ["client", "runtime", "ws"] }
|
||||||
k8s-openapi = { version = "0.26", features = ["v1_32"] }
|
k8s-openapi = { version = "0.26", features = ["v1_32"] }
|
||||||
|
chrono = "0.4"
|
||||||
|
futures = "0.3"
|
||||||
|
|||||||
36
nix/homelab/api/src/endpoints/kubernetes.rs
Normal file
36
nix/homelab/api/src/endpoints/kubernetes.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use actix_web::{HttpResponse, web};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::kubernetes;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct RestoreRequest {
|
||||||
|
pub backup_file: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RestoreResponse {
|
||||||
|
pub server: String,
|
||||||
|
pub job_name: String,
|
||||||
|
pub backup_file: String,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /api/minecraft/{server}/restore
|
||||||
|
pub async fn create_restore(
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
path: web::Path<ServerPath>,
|
||||||
|
body: web::Json<RestoreRequest>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
let job_name =
|
||||||
|
kubernetes::create_restore_job(&app_state.kube, &path.server, &body.backup_file).await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(RestoreResponse {
|
||||||
|
server: path.server.clone(),
|
||||||
|
job_name,
|
||||||
|
backup_file: body.backup_file.clone(),
|
||||||
|
status: "created".to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -1 +1,5 @@
|
|||||||
pub mod server_stats;
|
pub mod server_stats;
|
||||||
|
pub mod server_uptime;
|
||||||
|
pub mod world_size;
|
||||||
|
|
||||||
|
pub(self) const MINECRAFT_NAMESPACE: &str = "minecraft";
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
use actix_web::{HttpResponse, web};
|
use actix_web::{HttpResponse, web};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{AppState, error::Result, rcon::parse_online_list};
|
use crate::{
|
||||||
|
AppState,
|
||||||
|
endpoints::server_uptime::{PodUptime, get_pod_uptime},
|
||||||
|
error::Result,
|
||||||
|
rcon::parse_online_list,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct ServerStats {
|
struct ServerStats {
|
||||||
pub status: String,
|
pub status: String,
|
||||||
pub players_online: u16,
|
pub players_online: u16,
|
||||||
pub max_players: u16,
|
pub max_players: u16,
|
||||||
pub uptime: Option<String>,
|
pub uptime: PodUptime,
|
||||||
pub world_size: Option<String>,
|
pub world_size: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,12 +22,13 @@ pub async fn get_server_stats(app_state: web::Data<AppState>) -> Result<HttpResp
|
|||||||
|
|
||||||
let (_, (players_online, max_players)) =
|
let (_, (players_online, max_players)) =
|
||||||
parse_online_list(&list_response).map_err(|e| crate::error::Error::Parse(e.to_string()))?;
|
parse_online_list(&list_response).map_err(|e| crate::error::Error::Parse(e.to_string()))?;
|
||||||
|
let uptime = get_pod_uptime(&app_state.kube, "main").await?;
|
||||||
|
|
||||||
let stats = ServerStats {
|
let stats = ServerStats {
|
||||||
status: "Online".to_string(),
|
status: "Online".to_string(),
|
||||||
players_online,
|
players_online,
|
||||||
max_players,
|
max_players,
|
||||||
uptime: None,
|
uptime,
|
||||||
world_size: None,
|
world_size: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
56
nix/homelab/api/src/endpoints/server_uptime.rs
Normal file
56
nix/homelab/api/src/endpoints/server_uptime.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use actix_web::{HttpResponse, web};
|
||||||
|
use k8s_openapi::api::core::v1::Pod;
|
||||||
|
use kube::{Api, Client, api::ListParams};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
AppState,
|
||||||
|
endpoints::MINECRAFT_NAMESPACE,
|
||||||
|
error::{Error, Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub(super) struct PodUptime {
|
||||||
|
pub seconds: i64,
|
||||||
|
pub started_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_uptime(
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
path: web::Path<String>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
let server = path.into_inner();
|
||||||
|
let uptime = get_pod_uptime(&app_state.kube, &server).await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(uptime))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn find_minecraft_pod(client: &Client, server_name: &str) -> Result<Pod> {
|
||||||
|
let pods: Api<Pod> = Api::namespaced(client.clone(), MINECRAFT_NAMESPACE);
|
||||||
|
|
||||||
|
let label_selector = format!("app=minecraft-{server_name}");
|
||||||
|
let lp = ListParams::default().labels(&label_selector);
|
||||||
|
|
||||||
|
let pod_list = pods.list(&lp).await?;
|
||||||
|
|
||||||
|
pod_list
|
||||||
|
.items
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| Error::PodNotFound(format!("minecraft-{server_name}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn get_pod_uptime(client: &Client, server_name: &str) -> Result<PodUptime> {
|
||||||
|
let pod = find_minecraft_pod(client, server_name).await?;
|
||||||
|
|
||||||
|
let start_time = pod
|
||||||
|
.status
|
||||||
|
.and_then(|s| s.start_time)
|
||||||
|
.ok_or_else(|| Error::PodExec("pod has no start time".into()))?;
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
let duration = now.signed_duration_since(&start_time.0);
|
||||||
|
|
||||||
|
Ok(PodUptime {
|
||||||
|
seconds: duration.num_seconds(),
|
||||||
|
started_at: start_time.0.to_rfc3339(),
|
||||||
|
})
|
||||||
|
}
|
||||||
56
nix/homelab/api/src/endpoints/world_size.rs
Normal file
56
nix/homelab/api/src/endpoints/world_size.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use actix_web::{HttpResponse, web};
|
||||||
|
use k8s_openapi::api::core::v1::Pod;
|
||||||
|
use kube::{Api, Client};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
AppState,
|
||||||
|
endpoints::{MINECRAFT_NAMESPACE, server_uptime::find_minecraft_pod},
|
||||||
|
error::{Error, Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct WorldSizeResponse {
|
||||||
|
pub server: String,
|
||||||
|
pub size: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_world_size(
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
path: web::Path<String>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
let server = path.into_inner();
|
||||||
|
let size = get_size_inner(&app_state.kube, &server).await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(WorldSizeResponse {
|
||||||
|
server: server.clone(),
|
||||||
|
size,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_size_inner(client: &Client, server_name: &str) -> Result<String> {
|
||||||
|
let pod = find_minecraft_pod(client, server_name).await?;
|
||||||
|
let pod_name = pod
|
||||||
|
.metadata
|
||||||
|
.name
|
||||||
|
.ok_or_else(|| Error::PodNotFound("no pod name".into()))?;
|
||||||
|
|
||||||
|
let output = exec_in_pod(client, &pod_name, vec!["du", "-sh", "/data"]).await?;
|
||||||
|
|
||||||
|
let size = output
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exec_in_pod(client: &Client, pod_name: &str, command: &str) {
|
||||||
|
let pods: Api<Pod> = Api::namespaced(client.clone(), MINECRAFT_NAMESPACE);
|
||||||
|
|
||||||
|
const params = AttachParams {
|
||||||
|
stdin: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,12 +9,20 @@ pub enum Error {
|
|||||||
Rcon(#[from] rcon::Error),
|
Rcon(#[from] rcon::Error),
|
||||||
#[error("parse error: {0}")]
|
#[error("parse error: {0}")]
|
||||||
Parse(String),
|
Parse(String),
|
||||||
|
#[error("kubernetes error: {0}")]
|
||||||
|
Kube(#[from] kube::Error),
|
||||||
|
#[error("pod not found: {0}")]
|
||||||
|
PodNotFound(String),
|
||||||
|
#[error("pod exec error: {0}")]
|
||||||
|
PodExec(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for Error {
|
impl ResponseError for Error {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
match self {
|
match self {
|
||||||
Self::Rcon(_) => StatusCode::SERVICE_UNAVAILABLE,
|
Self::Rcon(_) => StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
Self::PodNotFound(_) => StatusCode::NOT_FOUND,
|
||||||
|
Self::Kube(_) | Self::PodExec(_) => StatusCode::BAD_GATEWAY,
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
219
nix/homelab/api/src/kubernetes.rs
Normal file
219
nix/homelab/api/src/kubernetes.rs
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
use futures::TryStreamExt;
|
||||||
|
use k8s_openapi::api::batch::v1::Job;
|
||||||
|
use k8s_openapi::api::core::v1::Pod;
|
||||||
|
use kube::Client;
|
||||||
|
use kube::api::{Api, AttachParams, ListParams, PostParams};
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
|
const NFS_SERVER: &str = "192.168.27.2";
|
||||||
|
const NFS_BACKUP_PATH: &str = "/backup/minecraft";
|
||||||
|
|
||||||
|
/// Find the Minecraft server pod by server name (e.g., "main", "creative")
|
||||||
|
|
||||||
|
/// Execute a command in a pod and return stdout
|
||||||
|
pub async fn exec_in_pod(client: &Client, pod_name: &str, command: Vec<&str>) -> Result<String> {
|
||||||
|
let pods: Api<Pod> = Api::namespaced(client.clone(), MINECRAFT_NAMESPACE);
|
||||||
|
|
||||||
|
let ap = AttachParams {
|
||||||
|
stdout: true,
|
||||||
|
stderr: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut attached = pods.exec(pod_name, command, &ap).await?;
|
||||||
|
|
||||||
|
let mut stdout_str = String::new();
|
||||||
|
if let Some(stdout) = attached.stdout() {
|
||||||
|
let bytes: Vec<u8> = tokio_util::io::ReaderStream::new(stdout)
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::PodExec(e.to_string()))?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect();
|
||||||
|
stdout_str = String::from_utf8_lossy(&bytes).to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for exec to finish
|
||||||
|
attached
|
||||||
|
.join()
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::PodExec(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(stdout_str.trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the world size by running du -sh /data in the pod
|
||||||
|
/// Get pod uptime by calculating time since pod started
|
||||||
|
|
||||||
|
/// Create a restore job for a Minecraft server
|
||||||
|
pub async fn create_restore_job(
|
||||||
|
client: &Client,
|
||||||
|
server_name: &str,
|
||||||
|
backup_file: &str,
|
||||||
|
) -> Result<String> {
|
||||||
|
let jobs: Api<Job> = Api::namespaced(client.clone(), MINECRAFT_NAMESPACE);
|
||||||
|
|
||||||
|
let job = build_restore_job(server_name, backup_file);
|
||||||
|
let job_name = job
|
||||||
|
.metadata
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "unknown".into());
|
||||||
|
|
||||||
|
jobs.create(&PostParams::default(), &job).await?;
|
||||||
|
|
||||||
|
Ok(job_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_restore_job(server_name: &str, backup_file: &str) -> Job {
|
||||||
|
use k8s_openapi::api::core::v1::{
|
||||||
|
Container, EnvVar, NFSVolumeSource, PersistentVolumeClaimVolumeSource, PodSecurityContext,
|
||||||
|
PodSpec, PodTemplateSpec, Volume, VolumeMount,
|
||||||
|
};
|
||||||
|
|
||||||
|
let job_name = format!(
|
||||||
|
"minecraft-restore-{server_name}-{}",
|
||||||
|
chrono::Utc::now().timestamp()
|
||||||
|
);
|
||||||
|
|
||||||
|
let restore_script = r#"
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Minecraft World Restore Job"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
SERVER_NAME="${SERVER_NAME}"
|
||||||
|
BACKUP_FILE="${BACKUP_FILE:-latest.tgz}"
|
||||||
|
BACKUP_PATH="/backups/${BACKUP_FILE}"
|
||||||
|
DATA_DIR="/data"
|
||||||
|
|
||||||
|
echo "Configuration:"
|
||||||
|
echo " Server: ${SERVER_NAME}"
|
||||||
|
echo " Backup file: ${BACKUP_FILE}"
|
||||||
|
echo " Backup path: ${BACKUP_PATH}"
|
||||||
|
echo " Data directory: ${DATA_DIR}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ ! -f "${BACKUP_PATH}" ]; then
|
||||||
|
echo "ERROR: Backup file not found: ${BACKUP_PATH}"
|
||||||
|
echo ""
|
||||||
|
echo "Available backups:"
|
||||||
|
ls -lh /backups/ | grep "minecraft-${SERVER_NAME}" || echo " (none found)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Backup file found"
|
||||||
|
echo " Size: $(du -hL ${BACKUP_PATH} | cut -f1)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ -d "${DATA_DIR}/world" ]; then
|
||||||
|
echo "WARNING: Existing world data found!"
|
||||||
|
echo "Removing existing world data..."
|
||||||
|
rm -rf "${DATA_DIR}/world" "${DATA_DIR}/world_nether" "${DATA_DIR}/world_the_end"
|
||||||
|
echo "Old world data removed"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Extracting backup..."
|
||||||
|
tar -xzf "${BACKUP_PATH}" -C "${DATA_DIR}/"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Restore Complete!"
|
||||||
|
echo "=========================================="
|
||||||
|
"#;
|
||||||
|
|
||||||
|
Job {
|
||||||
|
metadata: kube::core::ObjectMeta {
|
||||||
|
name: Some(job_name),
|
||||||
|
namespace: Some(MINECRAFT_NAMESPACE.to_string()),
|
||||||
|
labels: Some(
|
||||||
|
[("app".to_string(), "minecraft-restore".to_string())]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: Some(k8s_openapi::api::batch::v1::JobSpec {
|
||||||
|
backoff_limit: Some(0),
|
||||||
|
ttl_seconds_after_finished: Some(3600),
|
||||||
|
template: PodTemplateSpec {
|
||||||
|
metadata: Some(kube::core::ObjectMeta {
|
||||||
|
labels: Some(
|
||||||
|
[("app".to_string(), "minecraft-restore".to_string())]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
spec: Some(PodSpec {
|
||||||
|
restart_policy: Some("Never".to_string()),
|
||||||
|
security_context: Some(PodSecurityContext {
|
||||||
|
fs_group: Some(2000),
|
||||||
|
run_as_user: Some(1000),
|
||||||
|
run_as_group: Some(3000),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
containers: vec![Container {
|
||||||
|
name: "restore".to_string(),
|
||||||
|
image: Some("busybox:latest".to_string()),
|
||||||
|
command: Some(vec!["sh".to_string(), "-c".to_string()]),
|
||||||
|
args: Some(vec![restore_script.to_string()]),
|
||||||
|
env: Some(vec![
|
||||||
|
EnvVar {
|
||||||
|
name: "SERVER_NAME".to_string(),
|
||||||
|
value: Some(server_name.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnvVar {
|
||||||
|
name: "BACKUP_FILE".to_string(),
|
||||||
|
value: Some(backup_file.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
volume_mounts: Some(vec![
|
||||||
|
VolumeMount {
|
||||||
|
name: "data".to_string(),
|
||||||
|
mount_path: "/data".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
VolumeMount {
|
||||||
|
name: "backups".to_string(),
|
||||||
|
mount_path: "/backups".to_string(),
|
||||||
|
read_only: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
volumes: Some(vec![
|
||||||
|
Volume {
|
||||||
|
name: "data".to_string(),
|
||||||
|
persistent_volume_claim: Some(PersistentVolumeClaimVolumeSource {
|
||||||
|
claim_name: format!("minecraft-{server_name}-datadir"),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Volume {
|
||||||
|
name: "backups".to_string(),
|
||||||
|
nfs: Some(NFSVolumeSource {
|
||||||
|
server: NFS_SERVER.to_string(),
|
||||||
|
path: NFS_BACKUP_PATH.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,13 @@ mod rcon;
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use actix_web::{App, HttpServer, web};
|
use actix_web::{App, HttpServer, web};
|
||||||
|
use kube::Client;
|
||||||
|
|
||||||
use crate::rcon::RconClient;
|
use crate::rcon::RconClient;
|
||||||
|
|
||||||
struct AppState {
|
pub struct AppState {
|
||||||
rcon: RconClient,
|
rcon: RconClient,
|
||||||
|
kube: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Env {
|
struct Env {
|
||||||
@@ -36,8 +38,16 @@ fn load_env() -> Env {
|
|||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
let env = load_env();
|
let env = load_env();
|
||||||
|
|
||||||
|
// Initialize Kubernetes client
|
||||||
|
// Uses in-cluster config when running in k3s, falls back to ~/.kube/config locally
|
||||||
|
let kube_client = Client::try_default()
|
||||||
|
.await
|
||||||
|
.expect("failed to create Kubernetes client");
|
||||||
|
|
||||||
let app_state = web::Data::new(AppState {
|
let app_state = web::Data::new(AppState {
|
||||||
rcon: RconClient::new(env.rcon_password),
|
rcon: RconClient::new(env.rcon_password),
|
||||||
|
kube: kube_client,
|
||||||
});
|
});
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
@@ -48,10 +58,24 @@ async fn main() -> std::io::Result<()> {
|
|||||||
web::get()
|
web::get()
|
||||||
.to(async || concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))),
|
.to(async || concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))),
|
||||||
)
|
)
|
||||||
.service(web::scope("/api").route(
|
.service(
|
||||||
|
web::scope("/api")
|
||||||
|
.route(
|
||||||
"/minecraft-server-stats",
|
"/minecraft-server-stats",
|
||||||
web::get().to(endpoints::server_stats::get_server_stats),
|
web::get().to(endpoints::server_stats::get_server_stats),
|
||||||
))
|
)
|
||||||
|
// .route(
|
||||||
|
// "/minecraft/{server}/world-size",
|
||||||
|
// web::get().to(endpoints::kubernetes::get_world_size),
|
||||||
|
// )
|
||||||
|
.route(
|
||||||
|
"/minecraft/{server}/uptime",
|
||||||
|
web::get().to(endpoints::server_uptime::get_uptime),
|
||||||
|
), // .route(
|
||||||
|
// "/minecraft/{server}/restore",
|
||||||
|
// web::post().to(endpoints::kubernetes::create_restore),
|
||||||
|
// ),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.bind(("127.0.0.1", 8080))?
|
.bind(("127.0.0.1", 8080))?
|
||||||
.run()
|
.run()
|
||||||
|
|||||||
2
nix/homelab/frontend/.gitignore
vendored
2
nix/homelab/frontend/.gitignore
vendored
@@ -6,6 +6,8 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
/.svelte-kit
|
||||||
|
build
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
{ plugins: [prettier-plugin-svelte] }
|
{
|
||||||
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||||
|
"tailwindStylesheet": "./src/routes/layout.css"
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,18 +10,21 @@
|
|||||||
"@tanstack/svelte-query": "^6.0.14",
|
"@tanstack/svelte-query": "^6.0.14",
|
||||||
"@tanstack/svelte-query-devtools": "^6.0.3",
|
"@tanstack/svelte-query-devtools": "^6.0.3",
|
||||||
"bits-ui": "^2.15.4",
|
"bits-ui": "^2.15.4",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
|
"@sveltejs/kit": "^2.49.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"@tsconfig/svelte": "^5.0.6",
|
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-svelte": "^3.4.1",
|
"prettier-plugin-svelte": "^3.4.1",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
"svelte": "^5.43.8",
|
"svelte": "^5.43.8",
|
||||||
"svelte-check": "^4.3.4",
|
"svelte-check": "^4.3.4",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "npm:rolldown-vite@7.2.5",
|
"vite": "^7.2.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -61,6 +64,8 @@
|
|||||||
|
|
||||||
"@oxc-project/types": ["@oxc-project/types@0.97.0", "", {}, "sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ=="],
|
"@oxc-project/types": ["@oxc-project/types@0.97.0", "", {}, "sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ=="],
|
||||||
|
|
||||||
|
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||||
|
|
||||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.50", "", { "os": "android", "cpu": "arm64" }, "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag=="],
|
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.50", "", { "os": "android", "cpu": "arm64" }, "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag=="],
|
||||||
|
|
||||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w=="],
|
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w=="],
|
||||||
@@ -91,8 +96,14 @@
|
|||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.50", "", {}, "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.50", "", {}, "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="],
|
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="],
|
||||||
|
|
||||||
|
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="],
|
||||||
|
|
||||||
|
"@sveltejs/kit": ["@sveltejs/kit@2.49.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-JFtOqDoU0DI/+QSG8qnq5bKcehVb3tCHhOG4amsSYth5/KgO4EkJvi42xSAiyKmXAAULW1/Zdb6lkgGEgSxdZg=="],
|
||||||
|
|
||||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
|
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
|
||||||
|
|
||||||
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="],
|
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="],
|
||||||
@@ -137,10 +148,10 @@
|
|||||||
|
|
||||||
"@tanstack/svelte-query-devtools": ["@tanstack/svelte-query-devtools@6.0.3", "", { "dependencies": { "@tanstack/query-devtools": "5.92.0", "esm-env": "^1.2.1" }, "peerDependencies": { "@tanstack/svelte-query": "^6.0.12", "svelte": "^5.25.0" } }, "sha512-AHc/vPiUWeMFXKvtrlZot7wIlsIm4z5vd0wDeQUKwE5XTfZODu0no1A4UCLzVnY2WpbBpakIEUMH+PmSMwYXKg=="],
|
"@tanstack/svelte-query-devtools": ["@tanstack/svelte-query-devtools@6.0.3", "", { "dependencies": { "@tanstack/query-devtools": "5.92.0", "esm-env": "^1.2.1" }, "peerDependencies": { "@tanstack/svelte-query": "^6.0.12", "svelte": "^5.25.0" } }, "sha512-AHc/vPiUWeMFXKvtrlZot7wIlsIm4z5vd0wDeQUKwE5XTfZODu0no1A4UCLzVnY2WpbBpakIEUMH+PmSMwYXKg=="],
|
||||||
|
|
||||||
"@tsconfig/svelte": ["@tsconfig/svelte@5.0.6", "", {}, "sha512-yGxYL0I9eETH1/DR9qVJey4DAsCdeau4a9wYPKuXfEhm8lFO8wg+LLYJjIpAm6Fw7HSlhepPhYPDop75485yWQ=="],
|
|
||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.10.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-+054pVMzVTmRQV8BhpGv3UyfZ2Llgl8rdpDTon+cUH9+na0ncBVXj3wTUKh14+Kiz18ziM3b4ikpP5/Pc0rQEQ=="],
|
"@types/node": ["@types/node@24.10.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-+054pVMzVTmRQV8BhpGv3UyfZ2Llgl8rdpDTon+cUH9+na0ncBVXj3wTUKh14+Kiz18ziM3b4ikpP5/Pc0rQEQ=="],
|
||||||
@@ -157,6 +168,10 @@
|
|||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||||
|
|
||||||
|
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
|
||||||
|
|
||||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||||
|
|
||||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||||
@@ -183,6 +198,8 @@
|
|||||||
|
|
||||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
|
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
|
||||||
|
|
||||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
|
||||||
@@ -215,6 +232,8 @@
|
|||||||
|
|
||||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||||
|
|
||||||
|
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||||
@@ -229,6 +248,8 @@
|
|||||||
|
|
||||||
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg=="],
|
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg=="],
|
||||||
|
|
||||||
|
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.2", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA=="],
|
||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
"rolldown": ["rolldown@1.0.0-beta.50", "", { "dependencies": { "@oxc-project/types": "=0.97.0", "@rolldown/pluginutils": "1.0.0-beta.50" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-x64": "1.0.0-beta.50", "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A=="],
|
"rolldown": ["rolldown@1.0.0-beta.50", "", { "dependencies": { "@oxc-project/types": "=0.97.0", "@rolldown/pluginutils": "1.0.0-beta.50" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-x64": "1.0.0-beta.50", "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A=="],
|
||||||
@@ -237,6 +258,10 @@
|
|||||||
|
|
||||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||||
|
|
||||||
|
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||||
|
|
||||||
|
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||||
@@ -255,6 +280,8 @@
|
|||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
|
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|||||||
@@ -7,14 +7,19 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "prettier --check ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"@tsconfig/svelte": "^5.0.6",
|
"@sveltejs/kit": "^2.49.1",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-svelte": "^3.4.1",
|
"prettier-plugin-svelte": "^3.4.1",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
"svelte": "^5.43.8",
|
"svelte": "^5.43.8",
|
||||||
"svelte-check": "^4.3.4",
|
"svelte-check": "^4.3.4",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
@@ -29,6 +34,7 @@
|
|||||||
"@tanstack/svelte-query": "^6.0.14",
|
"@tanstack/svelte-query": "^6.0.14",
|
||||||
"@tanstack/svelte-query-devtools": "^6.0.3",
|
"@tanstack/svelte-query-devtools": "^6.0.3",
|
||||||
"bits-ui": "^2.15.4",
|
"bits-ui": "^2.15.4",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"tailwindcss": "^4.1.18"
|
"tailwindcss": "^4.1.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
nix/homelab/frontend/src/app.d.ts
vendored
Normal file
13
nix/homelab/frontend/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -4,14 +4,15 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<link rel="stylesheet" href="/src/app.css" />
|
<!-- <link rel="stylesheet" href="/src/app.css" /> -->
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Homelab</title>
|
<!-- <title>Homelab</title> -->
|
||||||
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div id="app"></div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -2,6 +2,12 @@
|
|||||||
import { createQuery } from "@tanstack/svelte-query";
|
import { createQuery } from "@tanstack/svelte-query";
|
||||||
import { fetchStats } from "./stats";
|
import { fetchStats } from "./stats";
|
||||||
import { LoaderCircle } from "@lucide/svelte";
|
import { LoaderCircle } from "@lucide/svelte";
|
||||||
|
import {
|
||||||
|
formatDistance,
|
||||||
|
formatDistanceToNow,
|
||||||
|
formatDuration,
|
||||||
|
intervalToDuration,
|
||||||
|
} from "date-fns";
|
||||||
|
|
||||||
const query = createQuery(() => ({
|
const query = createQuery(() => ({
|
||||||
queryKey: ["minecraft-server-stats"],
|
queryKey: ["minecraft-server-stats"],
|
||||||
@@ -9,6 +15,21 @@
|
|||||||
refetchInterval: 10000,
|
refetchInterval: 10000,
|
||||||
staleTime: 5000,
|
staleTime: 5000,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const formatUptime = () => {
|
||||||
|
if (!query.data) {
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
const started_at = query.data.uptime.started_at;
|
||||||
|
const duration = intervalToDuration({ start: started_at, end: new Date() });
|
||||||
|
$inspect(duration);
|
||||||
|
if (!duration.days && (duration.hours ?? 0) < 1) {
|
||||||
|
return formatDistanceToNow(new Date(query.data.uptime.started_at));
|
||||||
|
}
|
||||||
|
return formatDuration(duration, {
|
||||||
|
format: ["days", "hours", "minutes"],
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet statItem(label: string, value?: string | number)}
|
{#snippet statItem(label: string, value?: string | number)}
|
||||||
@@ -21,7 +42,7 @@
|
|||||||
<div class="border border-zinc-600 rounded-lg p-4">
|
<div class="border border-zinc-600 rounded-lg p-4">
|
||||||
<h3 class="text-lg font-medium mb-3">
|
<h3 class="text-lg font-medium mb-3">
|
||||||
Server Stats {#if query.isError}
|
Server Stats {#if query.isError}
|
||||||
<span class="text-sm text-red-500"
|
<span class="ml-1 text-sm text-red-500"
|
||||||
>an error occured fetching server stats</span
|
>an error occured fetching server stats</span
|
||||||
>
|
>
|
||||||
{:else if query.isPending}
|
{:else if query.isPending}
|
||||||
@@ -31,7 +52,7 @@
|
|||||||
<div class="grid grid-cols-2 gap-4 text-zinc-400">
|
<div class="grid grid-cols-2 gap-4 text-zinc-400">
|
||||||
{@render statItem("Players Online", query.data?.players_online ?? "--")}
|
{@render statItem("Players Online", query.data?.players_online ?? "--")}
|
||||||
{@render statItem("Server Status", query.data?.status ?? "--")}
|
{@render statItem("Server Status", query.data?.status ?? "--")}
|
||||||
{@render statItem("Uptime", query.data?.uptime ?? "--")}
|
{@render statItem("Uptime", formatUptime())}
|
||||||
{@render statItem("World Size", query.data?.world_size ?? "--")}
|
{@render statItem("World Size", query.data?.world_size ?? "--")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ type StatsResponse = {
|
|||||||
status: string;
|
status: string;
|
||||||
players_online: number;
|
players_online: number;
|
||||||
max_players: number;
|
max_players: number;
|
||||||
uptime: string;
|
uptime: {
|
||||||
|
seconds: number;
|
||||||
|
started_at: string;
|
||||||
|
};
|
||||||
world_size: string;
|
world_size: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchStats = async () => {
|
export const fetchStats = async () => {
|
||||||
const response = await fetch(
|
const response = await fetch("/api/minecraft-server-stats");
|
||||||
"http://localhost:5173/api/minecraft-server-stats",
|
|
||||||
);
|
|
||||||
return (await response.json()) as StatsResponse;
|
return (await response.json()) as StatsResponse;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { mount } from 'svelte'
|
|
||||||
import './app.css'
|
|
||||||
import App from './App.svelte'
|
|
||||||
|
|
||||||
const app = mount(App, {
|
|
||||||
target: document.getElementById('app')!,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default app
|
|
||||||
7
nix/homelab/frontend/src/routes/+layout.svelte
Normal file
7
nix/homelab/frontend/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import "./layout.css";
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
2
nix/homelab/frontend/src/routes/+layout.ts
Normal file
2
nix/homelab/frontend/src/routes/+layout.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const ssr = false;
|
||||||
|
export const prerender = false;
|
||||||
@@ -1,26 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { QueryClient, QueryClientProvider } from "@tanstack/svelte-query";
|
||||||
createQuery,
|
import Actions from "$lib/minecraft/Actions.svelte";
|
||||||
QueryClient,
|
import Stats from "$lib/minecraft/Stats.svelte";
|
||||||
QueryClientProvider,
|
|
||||||
} from "@tanstack/svelte-query";
|
|
||||||
import Actions from "./lib/minecraft/Actions.svelte";
|
|
||||||
import Stats from "./lib/minecraft/Stats.svelte";
|
|
||||||
import { SvelteQueryDevtools } from "@tanstack/svelte-query-devtools";
|
import { SvelteQueryDevtools } from "@tanstack/svelte-query-devtools";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<main class="flex justify-center mt-8">
|
<main class="mt-8 flex justify-center">
|
||||||
<div class="w-full max-w-2xl px-4">
|
<div class="w-full max-w-2xl px-4">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<h1 class="text-2xl font-semibold mb-8">Management</h1>
|
<h1 class="mb-8 text-2xl font-semibold">Management</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Minecraft Section -->
|
<!-- Minecraft Section -->
|
||||||
<section class="bg-zinc-800 rounded-lg p-6">
|
<section class="rounded-lg bg-zinc-800 p-6">
|
||||||
<h2 class="text-xl mb-4">Minecraft</h2>
|
<h2 class="mb-4 text-xl">Minecraft</h2>
|
||||||
<Actions />
|
<Actions />
|
||||||
<Stats />
|
<Stats />
|
||||||
</section>
|
</section>
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,8 +1,14 @@
|
|||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
import adapter from "@sveltejs/adapter-static";
|
||||||
|
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
|
||||||
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
|
/** @type {import("@sveltejs/kit").Config} */
|
||||||
export default {
|
export default {
|
||||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
}
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
fallback: "index.html",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
||||||
"target": "ES2022",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"types": ["svelte", "vite/client"],
|
|
||||||
"noEmit": true,
|
|
||||||
/**
|
|
||||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
|
||||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
|
||||||
* Note that setting allowJs false does not prevent the use
|
|
||||||
* of JS in `.svelte` files.
|
|
||||||
*/
|
|
||||||
"allowJs": true,
|
|
||||||
"checkJs": true,
|
|
||||||
"moduleDetection": "force"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,21 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
"references": [
|
"compilerOptions": {
|
||||||
{ "path": "./tsconfig.app.json" },
|
"rewriteRelativeImportExtensions": true,
|
||||||
{ "path": "./tsconfig.node.json" }
|
"allowJs": true,
|
||||||
]
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
}
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||||
|
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
||||||
"target": "ES2023",
|
|
||||||
"lib": ["ES2023"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"types": ["node"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [svelte(), tailwindcss()],
|
plugins: [sveltekit(), tailwindcss()],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
|
|||||||
Reference in New Issue
Block a user