diff --git a/.gitignore b/.gitignore index 46b5d68..0816e4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target .envrc + +/public/frontend diff --git a/.sqlx/query-110727bf8db2b15e50a755397eef76df66ef4963fe7dd8e6818d778de132ab7c.json b/.sqlx/query-110727bf8db2b15e50a755397eef76df66ef4963fe7dd8e6818d778de132ab7c.json index d13ed28..b0576dd 100644 --- a/.sqlx/query-110727bf8db2b15e50a755397eef76df66ef4963fe7dd8e6818d778de132ab7c.json +++ b/.sqlx/query-110727bf8db2b15e50a755397eef76df66ef4963fe7dd8e6818d778de132ab7c.json @@ -27,6 +27,11 @@ "ordinal": 4, "name": "created_at", "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "duration_nanos", + "type_info": "Int8" } ], "parameters": { @@ -39,7 +44,8 @@ false, false, true, - false + false, + true ] }, "hash": "110727bf8db2b15e50a755397eef76df66ef4963fe7dd8e6818d778de132ab7c" diff --git a/.sqlx/query-29c3cfac36bc7e37679121d24a7a008a7bd66b76f9df17d88f8218af5e6bb645.json b/.sqlx/query-cea15767c02453c0fdac99f24eece2959741ccd15196c7db0ed90a453248caf0.json similarity index 57% rename from .sqlx/query-29c3cfac36bc7e37679121d24a7a008a7bd66b76f9df17d88f8218af5e6bb645.json rename to .sqlx/query-cea15767c02453c0fdac99f24eece2959741ccd15196c7db0ed90a453248caf0.json index 146f66a..41982f9 100644 --- a/.sqlx/query-29c3cfac36bc7e37679121d24a7a008a7bd66b76f9df17d88f8218af5e6bb645.json +++ b/.sqlx/query-cea15767c02453c0fdac99f24eece2959741ccd15196c7db0ed90a453248caf0.json @@ -1,16 +1,17 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO Checkins (page_id, status_code, outcome)\n VALUES ($1, $2, $3)\n ", + "query": "\n INSERT INTO Checkins (page_id, status_code, outcome, duration_nanos)\n VALUES ($1, $2, $3, $4)\n ", "describe": { "columns": [], "parameters": { "Left": [ "Uuid", "Int4", - "Text" + "Text", + "Int8" ] }, "nullable": [] }, - "hash": "29c3cfac36bc7e37679121d24a7a008a7bd66b76f9df17d88f8218af5e6bb645" + "hash": "cea15767c02453c0fdac99f24eece2959741ccd15196c7db0ed90a453248caf0" } diff --git a/Cargo.lock b/Cargo.lock index f48de03..2307364 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -398,7 +398,7 @@ dependencies = [ [[package]] name = "cja" version = "0.0.0" -source = "git+https://github.com/coreyja/coreyja.com?branch=main#e03af1b4e33f890af46aa5fcd5e70551904ed895" +source = "git+https://github.com/coreyja/coreyja.com?branch=main#11eaf5e2c78165371738067a7f6afaec5340671e" dependencies = [ "async-trait", "axum-core 0.4.3", @@ -818,6 +818,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "h2" version = "0.3.22" @@ -1008,6 +1014,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.28" @@ -1128,6 +1140,26 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "include_dir" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" +dependencies = [ + "glob", + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1377,6 +1409,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2643,36 +2685,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "status" -version = "0.1.0" -dependencies = [ - "async-trait", - "axum 0.7.4", - "axum-macros", - "chrono", - "cja", - "futures", - "jsonwebtoken", - "maud", - "miette", - "opentelemetry", - "opentelemetry-otlp", - "reqwest", - "sentry", - "sentry-tower", - "serde", - "serde_json", - "sqlx", - "tokio", - "tower-cookies", - "tracing", - "tracing-opentelemetry", - "tracing-subscriber", - "tracing-tree", - "uuid", -] - [[package]] name = "stringprep" version = "0.1.4" @@ -3161,6 +3173,15 @@ dependencies = [ "libc", ] +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.14" @@ -3222,6 +3243,39 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "up_guardian" +version = "0.1.0" +dependencies = [ + "async-trait", + "axum 0.7.4", + "axum-macros", + "chrono", + "cja", + "futures", + "humantime", + "include_dir", + "jsonwebtoken", + "maud", + "miette", + "mime_guess", + "opentelemetry", + "opentelemetry-otlp", + "reqwest", + "sentry", + "sentry-tower", + "serde", + "serde_json", + "sqlx", + "tokio", + "tower-cookies", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", + "tracing-tree", + "uuid", +] + [[package]] name = "ureq" version = "2.9.1" diff --git a/Cargo.toml b/Cargo.toml index 57b0146..b286355 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "status" +name = "up_guardian" version = "0.1.0" edition = "2021" license = "private" +build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -53,3 +54,6 @@ futures = "0.3.30" sqlx = "0.7.3" uuid = { version = "1.6.1", features = ["v4"] } tower-cookies = { version = "0.10.0", features = ["private", "signed"] } +mime_guess = "2.0.4" +include_dir = { version = "0.7.3", features = ["metadata", "glob"] } +humantime = "2.1.0" diff --git a/Dockerfile b/Dockerfile index 0b71d4f..104f52b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,11 @@ COPY . . COPY tailwind.config.js . RUN ./tailwindcss -i src/tailwind.css -o target/tailwind.css -RUN cargo build --release --locked --bin status +RUN curl -fsSL https://bun.sh/install | BUN_INSTALL=/app bash + +ENV PATH="/app/bin:${PATH}" + +RUN cargo build --release --locked --bin up_guardian # Start building the final image FROM debian:stable-slim as final @@ -31,8 +35,8 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* \ && update-ca-certificates -COPY --from=builder /app/target/release/status . +COPY --from=builder /app/target/release/up_guardian . EXPOSE 3001 -ENTRYPOINT ["./status"] +ENTRYPOINT ["./up_guardian"] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..a5990fd --- /dev/null +++ b/build.rs @@ -0,0 +1,9 @@ +use std::process::Command; + +fn main() { + println!("cargo:rerun-if-changed=frontend/**/*"); + Command::new("bun") + .args(["build", "frontend/index.ts", "--outdir", "public/frontend/"]) + .status() + .expect("Failed to build frontend"); +} diff --git a/frontend/index.ts b/frontend/index.ts new file mode 100644 index 0000000..50799b0 --- /dev/null +++ b/frontend/index.ts @@ -0,0 +1,19 @@ +document.addEventListener("click", function (e) { + if (!e.target) return; + + const clickedElement = (e.target as HTMLElement).closest( + "[data-app='ToggleClass']" + ); + if (clickedElement) { + const className = clickedElement.getAttribute("data-class")!; + const targetSelector = clickedElement.getAttribute("data-target")!; + + const targetElement = + clickedElement.closest(targetSelector) || + document.querySelector(targetSelector); + + if (targetElement) { + targetElement.classList.toggle(className); + } + } +}); diff --git a/migrations/20240313231211_CreateCron.down.sql b/migrations/20240313231211_CreateCron.down.sql new file mode 100644 index 0000000..c3e03be --- /dev/null +++ b/migrations/20240313231211_CreateCron.down.sql @@ -0,0 +1 @@ +DROP TABLE Crons; diff --git a/migrations/20240313231211_CreateCron.up.sql b/migrations/20240313231211_CreateCron.up.sql new file mode 100644 index 0000000..0d9dd4d --- /dev/null +++ b/migrations/20240313231211_CreateCron.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE + Crons ( + cron_id UUID PRIMARY KEY, + name TEXT NOT NULL, + last_run_at TIMESTAMP + WITH + TIME ZONE NOT NULL, + created_at TIMESTAMP + WITH + TIME ZONE NOT NULL, + updated_at TIMESTAMP + WITH + TIME ZONE NOT NULL + ); + +CREATE UNIQUE INDEX idx_crons_name ON Crons (name); diff --git a/migrations/20240314020654_AddDurationToCheckin.down.sql b/migrations/20240314020654_AddDurationToCheckin.down.sql new file mode 100644 index 0000000..e2c9bc8 --- /dev/null +++ b/migrations/20240314020654_AddDurationToCheckin.down.sql @@ -0,0 +1,5 @@ +-- Add migration script here +-- Add a new column to the checkin table +-- `duration_ms` stores the duration of the checkin in milliseconds +ALTER TABLE Checkins +DROP COLUMN duration_nanos; diff --git a/migrations/20240314020654_AddDurationToCheckin.up.sql b/migrations/20240314020654_AddDurationToCheckin.up.sql new file mode 100644 index 0000000..023ac85 --- /dev/null +++ b/migrations/20240314020654_AddDurationToCheckin.up.sql @@ -0,0 +1,5 @@ +-- Add migration script here +-- Add a new column to the checkin table +-- `duration_ms` stores the duration of the checkin in milliseconds +ALTER TABLE Checkins +ADD COLUMN duration_nanos BIGINT; diff --git a/public/Logo.png b/public/Logo.png new file mode 100644 index 0000000..e391162 Binary files /dev/null and b/public/Logo.png differ diff --git a/public/Logomark.png b/public/Logomark.png new file mode 100644 index 0000000..a519dc6 Binary files /dev/null and b/public/Logomark.png differ diff --git a/src/jobs/create_checkin.rs b/src/jobs/create_checkin.rs index c6750c8..d418bb8 100644 --- a/src/jobs/create_checkin.rs +++ b/src/jobs/create_checkin.rs @@ -1,6 +1,9 @@ + use cja::{app_state::AppState as _, jobs::Job}; use miette::IntoDiagnostic; use serde::{Deserialize, Serialize}; + +use tokio::time::Instant; use uuid::Uuid; use crate::app_state::AppState; @@ -32,30 +35,33 @@ impl Job for CreateCheckin { let path = page.path; let url = format!("https://{domain}{path}"); + let now = Instant::now(); let resp = reqwest::get(&url).await; let (status, outcome) = match resp { Err(_) => (None, "error"), - Ok(resp) => ( - Some(resp.status()), + Ok(resp) => (Some(resp.status()), { if resp.status().is_success() { "success" } else { "failure" - }, - ), + } + }), }; + let duration = now.elapsed(); + let duration: i64 = duration.as_nanos().try_into().unwrap(); let status: Option = status.map(|s| s.as_u16().into()); sqlx::query!( r#" - INSERT INTO Checkins (page_id, status_code, outcome) - VALUES ($1, $2, $3) + INSERT INTO Checkins (page_id, status_code, outcome, duration_nanos) + VALUES ($1, $2, $3, $4) "#, self.page_id, status, - outcome + outcome, + duration ) .execute(app_state.db()) .await diff --git a/src/routes/current_user/pages.rs b/src/routes/current_user/pages.rs index 98aa91d..f27b69b 100644 --- a/src/routes/current_user/pages.rs +++ b/src/routes/current_user/pages.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use axum::{ extract::{Path, State}, response::{IntoResponse, Redirect}, @@ -125,10 +127,14 @@ pub async fn show( ul { @for checkin in checkins { li { - (checkin.created_at) " - " (checkin.outcome) + (checkin.created_at.format("%d/%m/%Y %H:%M:%S")) " - " (checkin.outcome) @if let Some(status) = checkin.status_code { " - " (status) } + @if let Some(duration) = checkin.duration_nanos { + @let duration = Duration::from_nanos(duration as u64); + " - " (humantime::format_duration(duration)) + } } } } diff --git a/src/routes/home.rs b/src/routes/home.rs index 71e0d14..7891188 100644 --- a/src/routes/home.rs +++ b/src/routes/home.rs @@ -13,7 +13,7 @@ pub async fn show(session: Option, State(state): State) -> script src="https://unpkg.com/htmx.org@1.9.10" {} link rel="stylesheet" href="/styles/tailwind.css" {} - title { "Status - Uptime Monitoring by coreyja" } + title { "UpGuardian - Uptime Monitoring by coreyja" } } body { diff --git a/src/routes/login.rs b/src/routes/login.rs index 495287a..30d09f3 100644 --- a/src/routes/login.rs +++ b/src/routes/login.rs @@ -20,7 +20,7 @@ pub async fn show(session: Option) -> axum::response::Response { if session.is_none() { let idp_url = std::env::var("COREYJA_IDP_URL").unwrap_or_else(|_| "https://coreyja.com".into()); - let login_url = format!("{}/login/status", idp_url); + let login_url = format!("{}/login/upguardian", idp_url); Redirect::temporary(&login_url).into_response() } else { Redirect::temporary("/").into_response() @@ -52,7 +52,7 @@ pub async fn callback( ) .unwrap(); - let claim_url = format!("{}/login/status", idp_url); + let claim_url = format!("{}/login/upguardian", idp_url); let resp = client .post(claim_url) diff --git a/src/routes/mod.rs b/src/routes/mod.rs index e73dd91..f259b3d 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,7 +1,11 @@ use axum::{ + extract::Path, + response::{IntoResponse as _, Response}, routing::{get, post}, Router, }; +use include_dir::Dir; +use miette::{IntoDiagnostic as _}; use crate::app_state::AppState; @@ -9,9 +13,37 @@ mod current_user; mod home; mod login; +const STATIC_ASSETS: Dir<'_> = include_dir::include_dir!("$CARGO_MANIFEST_DIR/public"); + +async fn static_assets(Path(p): Path) -> Response { + let path = p.strip_prefix('/').unwrap_or(&p); + let path = path.strip_suffix('/').unwrap_or(path); + + let entry = STATIC_ASSETS.get_file(path); + + let Some(entry) = entry else { + return ( + axum::http::StatusCode::NOT_FOUND, + format!("Static asset {path} not found"), + ) + .into_response(); + }; + + let mime = mime_guess::from_path(path).first_or_octet_stream(); + + let mut headers = axum::http::HeaderMap::new(); + headers.insert( + axum::http::header::CONTENT_TYPE, + mime.to_string().parse().into_diagnostic().unwrap(), + ); + + (headers, entry.contents()).into_response() +} + pub fn routes() -> Router { Router::new() .route("/", get(home::show)) + .route("/public/*path", get(static_assets)) .route("/styles/tailwind.css", get(tailwind_css)) .route("/login", get(login::show)) .route("/login/callback", get(login::callback)) diff --git a/src/setup.rs b/src/setup.rs index f3a6195..c3e493e 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -20,7 +20,7 @@ pub fn setup_tracing() -> Result<()> { let opentelemetry_layer = if let Ok(honeycomb_key) = std::env::var("HONEYCOMB_API_KEY") { let mut map = HashMap::::new(); map.insert("x-honeycomb-team".to_string(), honeycomb_key); - map.insert("x-honeycomb-dataset".to_string(), "status".to_string()); + map.insert("x-honeycomb-dataset".to_string(), "UpGuardian".to_string()); let tracer = opentelemetry_otlp::new_pipeline() .tracing() diff --git a/src/templates.rs b/src/templates.rs index cd2fe17..2782dee 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -21,17 +21,21 @@ impl IntoResponse for Template { link rel="stylesheet" href="/styles/tailwind.css" {} link rel="stylesheet" href=(format!("https://kit.fontawesome.com/{}.css", self.state.font_awesome_kit_id)) crossorigin="anonymous" {} - title { "Status - Uptime Monitoring by coreyja" } + script src="/public/frontend/index.js" {} + + meta name="viewport" content="width=device-width, initial-scale=1.0" {} + + title { "UpGuardian - Uptime Monitoring by coreyja" } } body class="h-full" { div { - div."relative z-50 lg:hidden" role="dialog" aria-modal="true" { + div."relative z-50 lg:hidden hidden" role="dialog" aria-modal="true" { div."fixed inset-0 bg-gray-900/80" {} div."fixed inset-0 flex" { div."relative mr-16 flex w-full max-w-xs flex-1" { div."absolute left-full top-0 flex w-16 justify-center pt-5" { - button."-m-2.5 p-2.5" type="button" { + button."-m-2.5 p-2.5" type="button" data-app="ToggleClass" data-class="hidden" data-target="div[role='dialog']" { span."sr-only" { "Close sidebar" } @@ -42,7 +46,9 @@ impl IntoResponse for Template { } div."flex grow flex-col gap-y-5 overflow-y-auto bg-indigo-600 px-6 pb-2" { div."flex h-16 shrink-0 items-center" { - img."h-8 w-auto" src="https://tailwindui.com/img/logos/mark.svg?color=white" alt="Status by Coreyja" {} + a href="/" { + img."h-12 w-auto mt-2" src="/public/Logomark.png" alt="UpGuardian by Coreyja" {} + } } nav."flex flex-1 flex-col" { ul."flex flex-1 flex-col gap-y-7" role="list" { @@ -146,10 +152,10 @@ impl IntoResponse for Template { // This is the sidebar for larger screens div."hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col" { div."flex grow flex-col gap-y-5 overflow-y-auto bg-indigo-600 px-6" { - div."flex h-16 shrink-0 items-center" { - // img."h-8 w-auto" src="https://tailwindui.com/img/logos/mark.svg?color=white" alt="Your Company" {} + div."flex shrink-0 items-center" { a href="/" { - h1."text-white text-2xl" { "Status" } + // h1."text-white text-2xl" { "UpGuardian" } + img."w-auto mt-4" src="/public/Logo.png" alt="UpGuradian by coreyja" {} } } nav."flex flex-1 flex-col" { @@ -202,7 +208,7 @@ impl IntoResponse for Template { } } div."sticky top-0 z-40 flex items-center gap-x-6 bg-indigo-600 px-4 py-4 shadow-sm sm:px-6 lg:hidden" { - button."-m-2.5 p-2.5 text-indigo-200 lg:hidden" type="button" { + button."-m-2.5 p-2.5 text-indigo-200 lg:hidden" type="button" data-app="ToggleClass" data-class="hidden" data-target="div[role='dialog']"{ span."sr-only" { "Open sidebar" }