mirror of
https://github.com/LittleQuartZ/addmon.git
synced 2026-02-07 02:45:28 +07:00
refactor: move Tauri window title to config and add mode toggle to header
This commit is contained in:
parent
0a4b289d70
commit
d19a7bdaa0
11 changed files with 489 additions and 65 deletions
31
apps/web/Dockerfile
Normal file
31
apps/web/Dockerfile
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
FROM node:22-alpine AS frontend-builder
|
||||||
|
WORKDIR /app
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml* ./
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
FROM rust:1.87-alpine AS backend-builder
|
||||||
|
RUN apk add --no-cache musl-dev
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY src-tauri/Cargo.toml src-tauri/Cargo.lock ./
|
||||||
|
COPY src-tauri/src ./src
|
||||||
|
COPY src-tauri/build.rs ./
|
||||||
|
|
||||||
|
RUN cargo build --release --bin addmon-server --features server --no-default-features
|
||||||
|
|
||||||
|
FROM alpine:3.20
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=backend-builder /app/target/release/addmon-server /app/addmon-server
|
||||||
|
COPY --from=frontend-builder /app/dist /app/dist
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["/app/addmon-server"]
|
||||||
177
apps/web/src-tauri/Cargo.lock
generated
177
apps/web/src-tauri/Cargo.lock
generated
|
|
@ -2,6 +2,26 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "addmon"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-build",
|
||||||
|
"tauri-plugin-axum",
|
||||||
|
"tauri-plugin-dialog",
|
||||||
|
"tauri-plugin-fs",
|
||||||
|
"tauri-plugin-log",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler2"
|
name = "adler2"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
|
|
@ -75,21 +95,6 @@ version = "1.0.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "app"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tauri",
|
|
||||||
"tauri-build",
|
|
||||||
"tauri-plugin-dialog",
|
|
||||||
"tauri-plugin-fs",
|
|
||||||
"tauri-plugin-log",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
|
|
@ -186,6 +191,58 @@ version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.8.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
||||||
|
dependencies = [
|
||||||
|
"axum-core",
|
||||||
|
"bytes",
|
||||||
|
"form_urlencoded",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"itoa",
|
||||||
|
"matchit",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"serde_core",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.21.7"
|
version = "0.21.7"
|
||||||
|
|
@ -1499,12 +1556,24 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-range-header"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
|
|
@ -1518,6 +1587,7 @@ dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
|
|
@ -1969,6 +2039,12 @@ version = "0.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.6"
|
version = "2.7.6"
|
||||||
|
|
@ -1990,6 +2066,16 @@ version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
|
|
@ -3267,6 +3353,17 @@ dependencies = [
|
||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_path_to_error"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_repr"
|
name = "serde_repr"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
|
|
@ -3795,6 +3892,25 @@ dependencies = [
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-axum"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "git+https://github.com/mcitem/tauri-plugin-axum#0ea5c02790d500c20352113984bf6584429ac3c0"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http-body-util",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-dialog"
|
name = "tauri-plugin-dialog"
|
||||||
version = "2.4.2"
|
version = "2.4.2"
|
||||||
|
|
@ -4089,13 +4205,26 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
"tracing",
|
"tracing",
|
||||||
"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.111",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.17"
|
version = "0.7.17"
|
||||||
|
|
@ -4218,6 +4347,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4228,14 +4358,24 @@ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"http-range-header",
|
||||||
|
"httpdate",
|
||||||
"iri-string",
|
"iri-string",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4256,6 +4396,7 @@ version = "0.1.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
|
|
@ -4373,6 +4514,12 @@ dependencies = [
|
||||||
"unic-common",
|
"unic-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,56 @@
|
||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "addmon"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "A Tauri App"
|
description = "Alert Monitoring App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
repository = ""
|
repository = ""
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.77.2"
|
rust-version = "1.77.2"
|
||||||
|
default-run = "addmon-desktop"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "app_lib"
|
name = "addmon_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "addmon-desktop"
|
||||||
|
path = "src/main.rs"
|
||||||
|
required-features = ["desktop"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "addmon-server"
|
||||||
|
path = "src/bin/server.rs"
|
||||||
|
required-features = ["server"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["desktop"]
|
||||||
|
desktop = [
|
||||||
|
"tauri",
|
||||||
|
"tauri-build",
|
||||||
|
"tauri-plugin-log",
|
||||||
|
"tauri-plugin-fs",
|
||||||
|
"tauri-plugin-dialog",
|
||||||
|
"tauri-plugin-axum",
|
||||||
|
]
|
||||||
|
server = ["tokio/full", "tower-http"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.5.3", features = [] }
|
tauri-build = { version = "2.5.3", features = [], optional = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
tauri = { version = "2.9.5", features = ["macos-private-api"] }
|
|
||||||
tauri-plugin-log = "2"
|
|
||||||
tauri-plugin-fs = "2"
|
|
||||||
tauri-plugin-dialog = "2"
|
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
axum = "0.8"
|
||||||
|
tower = "0.5"
|
||||||
|
|
||||||
|
tauri = { version = "2.9.5", features = ["macos-private-api"], optional = true }
|
||||||
|
tauri-plugin-log = { version = "2", optional = true }
|
||||||
|
tauri-plugin-fs = { version = "2", optional = true }
|
||||||
|
tauri-plugin-dialog = { version = "2", optional = true }
|
||||||
|
tauri-plugin-axum = { git = "https://github.com/mcitem/tauri-plugin-axum", optional = true }
|
||||||
|
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros"], default-features = false }
|
||||||
|
tower-http = { version = "0.6", features = ["fs", "cors"], optional = true }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
59
apps/web/src-tauri/src/api.rs
Normal file
59
apps/web/src-tauri/src/api.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
use axum::{
|
||||||
|
extract::DefaultBodyLimit,
|
||||||
|
http::StatusCode,
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::{get, post},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::{calculate_kpis, pair_alerts, GrafanaAlert, Incident, KpiMetrics, ProcessedAlert};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ProcessAlertsRequest {
|
||||||
|
json_content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_alerts_handler(
|
||||||
|
Json(req): Json<ProcessAlertsRequest>,
|
||||||
|
) -> Result<Json<Vec<ProcessedAlert>>, AppError> {
|
||||||
|
let raw_alerts: Vec<GrafanaAlert> =
|
||||||
|
serde_json::from_str(&req.json_content).map_err(|e| AppError::Json(e.to_string()))?;
|
||||||
|
Ok(Json(pair_alerts(raw_alerts)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CalculateKpisRequest {
|
||||||
|
alerts: Vec<ProcessedAlert>,
|
||||||
|
incidents: Vec<Incident>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn calculate_kpis_handler(Json(req): Json<CalculateKpisRequest>) -> Json<KpiMetrics> {
|
||||||
|
Json(calculate_kpis(&req.alerts, &req.incidents))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn health_handler() -> &'static str {
|
||||||
|
"ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/api/health", get(health_handler))
|
||||||
|
.route("/api/process-alerts", post(process_alerts_handler))
|
||||||
|
.route("/api/calculate-kpis", post(calculate_kpis_handler))
|
||||||
|
.layer(DefaultBodyLimit::max(50 * 1024 * 1024))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum AppError {
|
||||||
|
Json(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
let (status, message) = match self {
|
||||||
|
AppError::Json(msg) => (StatusCode::BAD_REQUEST, msg),
|
||||||
|
};
|
||||||
|
(status, Json(serde_json::json!({ "error": message }))).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
22
apps/web/src-tauri/src/bin/server.rs
Normal file
22
apps/web/src-tauri/src/bin/server.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
use addmon_lib::create_router;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use tower_http::services::{ServeDir, ServeFile};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let port: u16 = std::env::var("PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.parse().ok())
|
||||||
|
.unwrap_or(3000);
|
||||||
|
|
||||||
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||||
|
|
||||||
|
let api = create_router();
|
||||||
|
let static_files = ServeDir::new("dist").not_found_service(ServeFile::new("dist/index.html"));
|
||||||
|
let app = api.fallback_service(static_files);
|
||||||
|
|
||||||
|
println!("Server listening on http://{}", addr);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![cfg(feature = "desktop")]
|
||||||
|
|
||||||
use crate::{calculate_kpis, pair_alerts, AppError, GrafanaAlert, Incident, KpiMetrics, ProcessedAlert};
|
use crate::{calculate_kpis, pair_alerts, AppError, GrafanaAlert, Incident, KpiMetrics, ProcessedAlert};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
mod api;
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
mod commands;
|
mod commands;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -49,7 +51,6 @@ pub struct AlertData {
|
||||||
pub values: HashMap<String, f64>,
|
pub values: HashMap<String, f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deserialize metric values that may contain "+Inf", "-Inf", "NaN" as strings
|
|
||||||
fn deserialize_metric_values<'de, D>(deserializer: D) -> Result<HashMap<String, f64>, D::Error>
|
fn deserialize_metric_values<'de, D>(deserializer: D) -> Result<HashMap<String, f64>, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
|
|
@ -280,9 +281,15 @@ fn format_duration(ms: u64) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub use api::create_router;
|
||||||
|
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
|
let router = create_router();
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_axum::init(router))
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
app_lib::run();
|
addmon_lib::run();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
62
apps/web/src/lib/api/client.ts
Normal file
62
apps/web/src/lib/api/client.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import type { Incident, KpiMetrics, ProcessedAlert } from "@/lib/types/alerts";
|
||||||
|
|
||||||
|
const isTauri = (): boolean =>
|
||||||
|
typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
|
||||||
|
|
||||||
|
export async function processAlerts(
|
||||||
|
jsonContent: string,
|
||||||
|
): Promise<ProcessedAlert[]> {
|
||||||
|
if (isTauri()) {
|
||||||
|
const { invoke } = await import("@tauri-apps/api/core");
|
||||||
|
return invoke<ProcessedAlert[]>("process_alerts_json", { jsonContent });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch("/api/process-alerts", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ json_content: jsonContent }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response
|
||||||
|
.json()
|
||||||
|
.catch(() => ({ error: response.statusText }));
|
||||||
|
throw new Error(error.error ?? "Request failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function calculateKpis(
|
||||||
|
alerts: ProcessedAlert[],
|
||||||
|
incidents: Incident[],
|
||||||
|
): Promise<KpiMetrics> {
|
||||||
|
if (isTauri()) {
|
||||||
|
const { invoke } = await import("@tauri-apps/api/core");
|
||||||
|
return invoke<KpiMetrics>("calculate_kpis_command", { alerts, incidents });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch("/api/calculate-kpis", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ alerts, incidents }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response
|
||||||
|
.json()
|
||||||
|
.catch(() => ({ error: response.statusText }));
|
||||||
|
throw new Error(error.error ?? "Request failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function healthCheck(): Promise<string> {
|
||||||
|
if (isTauri()) {
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch("/api/health");
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { ArrowDownWideNarrow, FileUp, Filter, Search } from "lucide-react";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
|
||||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
|
||||||
import { FileUp, Filter, Search } from "lucide-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { AlertList } from "@/components/monitor/alert-list";
|
import { AlertList } from "@/components/monitor/alert-list";
|
||||||
|
|
@ -19,9 +16,11 @@ import {
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { calculateKpis, processAlerts } from "@/lib/api/client";
|
||||||
import type { Incident, KpiMetrics, ProcessedAlert } from "@/lib/types/alerts";
|
import type { Incident, KpiMetrics, ProcessedAlert } from "@/lib/types/alerts";
|
||||||
|
|
||||||
type StatusFilter = "all" | "resolved" | "unresolved";
|
type StatusFilter = "all" | "resolved" | "unresolved";
|
||||||
|
type SortBy = "alertTime" | "duration";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: MonitorPage,
|
component: MonitorPage,
|
||||||
|
|
@ -37,9 +36,10 @@ function MonitorPage() {
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = React.useState("");
|
const [searchQuery, setSearchQuery] = React.useState("");
|
||||||
const [statusFilter, setStatusFilter] = React.useState<StatusFilter>("all");
|
const [statusFilter, setStatusFilter] = React.useState<StatusFilter>("all");
|
||||||
|
const [sortBy, setSortBy] = React.useState<SortBy>("alertTime");
|
||||||
|
|
||||||
const filteredAlerts = React.useMemo(() => {
|
const filteredAlerts = React.useMemo(() => {
|
||||||
return alerts.filter((alert) => {
|
const filtered = alerts.filter((alert) => {
|
||||||
if (statusFilter === "resolved" && !alert.isResolved) return false;
|
if (statusFilter === "resolved" && !alert.isResolved) return false;
|
||||||
if (statusFilter === "unresolved" && alert.isResolved) return false;
|
if (statusFilter === "unresolved" && alert.isResolved) return false;
|
||||||
|
|
||||||
|
|
@ -54,7 +54,14 @@ function MonitorPage() {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}, [alerts, searchQuery, statusFilter]);
|
|
||||||
|
return filtered.sort((a, b) => {
|
||||||
|
if (sortBy === "duration") {
|
||||||
|
return b.durationMs - a.durationMs;
|
||||||
|
}
|
||||||
|
return b.alertTime - a.alertTime;
|
||||||
|
});
|
||||||
|
}, [alerts, searchQuery, statusFilter, sortBy]);
|
||||||
|
|
||||||
const selectAllState = React.useMemo(() => {
|
const selectAllState = React.useMemo(() => {
|
||||||
if (filteredAlerts.length === 0) return "none" as const;
|
if (filteredAlerts.length === 0) return "none" as const;
|
||||||
|
|
@ -67,10 +74,7 @@ function MonitorPage() {
|
||||||
const recalculateKpis = React.useCallback(
|
const recalculateKpis = React.useCallback(
|
||||||
async (currentAlerts: ProcessedAlert[], currentIncidents: Incident[]) => {
|
async (currentAlerts: ProcessedAlert[], currentIncidents: Incident[]) => {
|
||||||
try {
|
try {
|
||||||
const newKpis = await invoke<KpiMetrics>("calculate_kpis_command", {
|
const newKpis = await calculateKpis(currentAlerts, currentIncidents);
|
||||||
alerts: currentAlerts,
|
|
||||||
incidents: currentIncidents,
|
|
||||||
});
|
|
||||||
setKpis(newKpis);
|
setKpis(newKpis);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
|
@ -79,35 +83,65 @@ function MonitorPage() {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isTauri = "__TAURI_INTERNALS__" in window;
|
||||||
|
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleLoadFile = async () => {
|
const handleLoadFile = async () => {
|
||||||
|
if (isTauri) {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||||
|
const { readTextFile } = await import("@tauri-apps/plugin-fs");
|
||||||
|
|
||||||
|
const filePath = await open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [{ name: "JSON", extensions: ["json"] }],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await readTextFile(filePath);
|
||||||
|
const newAlerts = await processAlerts(content);
|
||||||
|
|
||||||
|
setAlerts(newAlerts);
|
||||||
|
setSelectedAlert(null);
|
||||||
|
await recalculateKpis(newAlerts, incidents);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileInputChange = async (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const filePath = await open({
|
const content = await file.text();
|
||||||
multiple: false,
|
const newAlerts = await processAlerts(content);
|
||||||
filters: [{ name: "JSON", extensions: ["json"] }],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!filePath) {
|
setAlerts(newAlerts);
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await readTextFile(filePath);
|
|
||||||
|
|
||||||
const processedAlerts = await invoke<ProcessedAlert[]>(
|
|
||||||
"process_alerts_json",
|
|
||||||
{ jsonContent: content },
|
|
||||||
);
|
|
||||||
|
|
||||||
setAlerts(processedAlerts);
|
|
||||||
setSelectedAlert(null);
|
setSelectedAlert(null);
|
||||||
await recalculateKpis(processedAlerts, incidents);
|
await recalculateKpis(newAlerts, incidents);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
e.target.value = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -184,15 +218,15 @@ function MonitorPage() {
|
||||||
);
|
);
|
||||||
const updatedIncidents = incidentId
|
const updatedIncidents = incidentId
|
||||||
? incidents.map((i) =>
|
? incidents.map((i) =>
|
||||||
i.id === incidentId
|
i.id === incidentId
|
||||||
? {
|
? {
|
||||||
...i,
|
...i,
|
||||||
attachedAlertIds: i.attachedAlertIds.filter(
|
attachedAlertIds: i.attachedAlertIds.filter(
|
||||||
(aid) => aid !== alertId,
|
(aid) => aid !== alertId,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: i,
|
: i,
|
||||||
)
|
)
|
||||||
: incidents;
|
: incidents;
|
||||||
|
|
||||||
setAlerts(updatedAlerts);
|
setAlerts(updatedAlerts);
|
||||||
|
|
@ -208,6 +242,13 @@ function MonitorPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col overflow-hidden">
|
<div className="flex h-screen flex-col overflow-hidden">
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
onChange={handleFileInputChange}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
<header className="flex shrink-0 items-center justify-between border-b px-4 py-3">
|
<header className="flex shrink-0 items-center justify-between border-b px-4 py-3">
|
||||||
<h1 className="text-sm font-semibold">Alert Monitor</h1>
|
<h1 className="text-sm font-semibold">Alert Monitor</h1>
|
||||||
<Button onClick={handleLoadFile} disabled={isLoading} size="sm">
|
<Button onClick={handleLoadFile} disabled={isLoading} size="sm">
|
||||||
|
|
@ -272,6 +313,31 @@ function MonitorPage() {
|
||||||
</DropdownMenuRadioGroup>
|
</DropdownMenuRadioGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
render={
|
||||||
|
<Button variant="ghost" size="sm" className="h-7 gap-1 px-2">
|
||||||
|
<ArrowDownWideNarrow className="size-3" />
|
||||||
|
<span className="text-xs">
|
||||||
|
{sortBy === "alertTime" ? "Newest" : "Longest"}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<DropdownMenuRadioGroup
|
||||||
|
value={sortBy}
|
||||||
|
onValueChange={(v) => setSortBy(v as SortBy)}
|
||||||
|
>
|
||||||
|
<DropdownMenuRadioItem value="alertTime">
|
||||||
|
Newest First
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
<DropdownMenuRadioItem value="duration">
|
||||||
|
Longest Duration
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
<div className="relative ml-auto flex-1 max-w-xs">
|
<div className="relative ml-auto flex-1 max-w-xs">
|
||||||
<Search className="absolute left-2 top-1/2 size-3 -translate-y-1/2 text-muted-foreground" />
|
<Search className="absolute left-2 top-1/2 size-3 -translate-y-1/2 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue