From bfcd868337337e2a0509acfde58511a6ca387ccd Mon Sep 17 00:00:00 2001 From: Xevion Date: Sat, 13 Sep 2025 19:34:34 -0500 Subject: [PATCH] refactor: proper implementation of services status, better styling/appearance/logic --- src/web/routes.rs | 91 ++++++++++++++++++++++++++-------- web/src/lib/api.ts | 28 ++++------- web/src/routes/index.tsx | 102 +++++++++++++++++++++------------------ 3 files changed, 134 insertions(+), 87 deletions(-) diff --git a/src/web/routes.rs b/src/web/routes.rs index df0a405..b5da3f4 100644 --- a/src/web/routes.rs +++ b/src/web/routes.rs @@ -9,8 +9,9 @@ use axum::{ routing::get, }; use http::header; +use serde::Serialize; use serde_json::{Value, json}; -use std::{sync::Arc, time::Duration}; +use std::{collections::BTreeMap, sync::Arc, time::Duration}; use tower_http::{ classify::ServerErrorsFailureClass, cors::{Any, CorsLayer}, @@ -211,30 +212,78 @@ async fn health() -> Json { })) } +#[derive(Serialize)] +enum Status { + Disabled, + Connected, + Active, + Healthy, + Error, +} + +#[derive(Serialize)] +struct ServiceInfo { + name: String, + status: Status, +} + +#[derive(Serialize)] +struct StatusResponse { + status: Status, + version: String, + commit: String, + services: BTreeMap, +} + /// Status endpoint showing bot and system status -async fn status(State(_state): State) -> Json { - // For now, return basic status without accessing private fields - Json(json!({ - "status": "operational", - "version": env!("CARGO_PKG_VERSION"), - "bot": { - "status": "running", - "uptime": "TODO: implement uptime tracking" +async fn status(State(_state): State) -> Json { + let mut services = BTreeMap::new(); + + // Bot service status - hardcoded as disabled for now + services.insert( + "bot".to_string(), + ServiceInfo { + name: "Bot".to_string(), + status: Status::Disabled, }, - "cache": { - "status": "connected", - "courses": "TODO: implement course counting", - "subjects": "TODO: implement subject counting" + ); + + // Banner API status - always connected for now + services.insert( + "banner".to_string(), + ServiceInfo { + name: "Banner".to_string(), + status: Status::Connected, }, - "banner_api": { - "status": "connected" + ); + + // Discord status - hardcoded as disabled for now + services.insert( + "discord".to_string(), + ServiceInfo { + name: "Discord".to_string(), + status: Status::Disabled, }, - "git": { - "commit": env!("GIT_COMMIT_HASH"), - "short": env!("GIT_COMMIT_SHORT") - }, - "timestamp": chrono::Utc::now().to_rfc3339() - })) + ); + + let overall_status = if services.values().any(|s| matches!(s.status, Status::Error)) { + Status::Error + } else if services + .values() + .all(|s| matches!(s.status, Status::Active | Status::Connected)) + { + Status::Active + } else { + // If we have any Disabled services but no errors, show as Healthy + Status::Healthy + }; + + Json(StatusResponse { + status: overall_status, + version: env!("CARGO_PKG_VERSION").to_string(), + commit: env!("GIT_COMMIT_HASH").to_string(), + services, + }) } /// Metrics endpoint for monitoring diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index e1aca20..9ea8ba8 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -6,26 +6,18 @@ export interface HealthResponse { timestamp: string; } +export type Status = "Disabled" | "Connected" | "Active" | "Healthy" | "Error"; + +export interface ServiceInfo { + name: string; + status: Status; +} + export interface StatusResponse { - status: string; + status: Status; version: string; - bot: { - status: string; - uptime: string; - }; - cache: { - status: string; - courses: string; - subjects: string; - }; - banner_api: { - status: string; - }; - git: { - commit: string; - short: string; - }; - timestamp: string; + commit: string; + services: Record; } export interface MetricsResponse { diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx index 3e12528..10bbdaa 100644 --- a/web/src/routes/index.tsx +++ b/web/src/routes/index.tsx @@ -1,17 +1,17 @@ import { createFileRoute } from "@tanstack/react-router"; import { useState, useEffect } from "react"; -import { apiClient, type StatusResponse } from "../lib/api"; +import { apiClient, type StatusResponse, type Status } from "../lib/api"; import { Card, Flex, Text, Tooltip } from "@radix-ui/themes"; import { CheckCircle, - AlertCircle, XCircle, Clock, Bot, - Database, Globe, Hourglass, Activity, + MessageCircle, + Circle, } from "lucide-react"; import TimeAgo from "react-timeago"; import "../App.css"; @@ -34,10 +34,14 @@ const BORDER_STYLES = { borderTop: "1px solid #e2e8f0", } as const; -// Types -type HealthStatus = "healthy" | "warning" | "error" | "unknown"; -type ServiceStatus = "running" | "connected" | "disconnected" | "error"; +// Service icon mapping +const SERVICE_ICONS: Record = { + bot: Bot, + banner: Globe, + discord: MessageCircle, +}; +// Types interface ResponseTiming { health: number | null; status: number | null; @@ -50,71 +54,72 @@ interface StatusIcon { interface Service { name: string; - status: ServiceStatus; + status: Status; icon: typeof Bot; } // Helper functions -const getStatusIcon = (status: string): StatusIcon => { - const statusMap: Record = { - healthy: { icon: CheckCircle, color: "green" }, - running: { icon: CheckCircle, color: "green" }, - connected: { icon: CheckCircle, color: "green" }, - warning: { icon: AlertCircle, color: "orange" }, - error: { icon: XCircle, color: "red" }, - disconnected: { icon: XCircle, color: "red" }, +const getStatusIcon = (status: Status): StatusIcon => { + const statusMap: Record = { + Active: { icon: CheckCircle, color: "green" }, + Connected: { icon: CheckCircle, color: "green" }, + Healthy: { icon: CheckCircle, color: "green" }, + Disabled: { icon: Circle, color: "gray" }, + Error: { icon: XCircle, color: "red" }, }; - return statusMap[status] || { icon: XCircle, color: "red" }; + return statusMap[status]; }; const getOverallHealth = ( status: StatusResponse | null, error: string | null -): HealthStatus => { - if (error) return "error"; - if (!status) return "unknown"; +): Status => { + if (error) return "Error"; + if (!status) return "Error"; - const allHealthy = - status.bot.status === "running" && - status.cache.status === "connected" && - status.banner_api.status === "connected"; - - return allHealthy ? "healthy" : "warning"; + return status.status; }; const getServices = (status: StatusResponse | null): Service[] => { if (!status) return []; - return [ - { name: "Bot", status: status.bot.status as ServiceStatus, icon: Bot }, - { - name: "Cache", - status: status.cache.status as ServiceStatus, - icon: Database, - }, - { - name: "Banner API", - status: status.banner_api.status as ServiceStatus, - icon: Globe, - }, - ]; + return Object.entries(status.services).map(([serviceId, serviceInfo]) => ({ + name: serviceInfo.name, + status: serviceInfo.status, + icon: SERVICE_ICONS[serviceId] || SERVICE_ICONS.default, + })); +}; + +// Status Component +const StatusDisplay = ({ status }: { status: Status }) => { + const { icon: Icon, color } = getStatusIcon(status); + + return ( + + + {status} + + + + ); }; // Service Status Component const ServiceStatus = ({ service }: { service: Service }) => { - const { icon: Icon, color } = getStatusIcon(service.status); - return ( {service.name} - - - {service.status} - + ); }; @@ -193,6 +198,7 @@ function App() { System Status + {/* Individual Services */} @@ -234,7 +240,7 @@ function App() { )} - {(status?.git?.commit || status?.version) && ( + {(status?.commit || status?.version) && ( )} - {status?.version && status?.git?.commit && ( + {status?.version && status?.commit && (
)} - {status?.git?.commit && ( + {status?.commit && (