refactor: proper implementation of services status, better styling/appearance/logic

This commit is contained in:
2025-09-13 19:34:34 -05:00
parent 99f0d0bc49
commit bfcd868337
3 changed files with 134 additions and 87 deletions

View File

@@ -9,8 +9,9 @@ use axum::{
routing::get, routing::get,
}; };
use http::header; use http::header;
use serde::Serialize;
use serde_json::{Value, json}; use serde_json::{Value, json};
use std::{sync::Arc, time::Duration}; use std::{collections::BTreeMap, sync::Arc, time::Duration};
use tower_http::{ use tower_http::{
classify::ServerErrorsFailureClass, classify::ServerErrorsFailureClass,
cors::{Any, CorsLayer}, cors::{Any, CorsLayer},
@@ -211,30 +212,78 @@ async fn health() -> Json<Value> {
})) }))
} }
#[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<String, ServiceInfo>,
}
/// Status endpoint showing bot and system status /// Status endpoint showing bot and system status
async fn status(State(_state): State<BannerState>) -> Json<Value> { async fn status(State(_state): State<BannerState>) -> Json<StatusResponse> {
// For now, return basic status without accessing private fields let mut services = BTreeMap::new();
Json(json!({
"status": "operational", // Bot service status - hardcoded as disabled for now
"version": env!("CARGO_PKG_VERSION"), services.insert(
"bot": { "bot".to_string(),
"status": "running", ServiceInfo {
"uptime": "TODO: implement uptime tracking" name: "Bot".to_string(),
status: Status::Disabled,
}, },
"cache": { );
"status": "connected",
"courses": "TODO: implement course counting", // Banner API status - always connected for now
"subjects": "TODO: implement subject counting" 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") let overall_status = if services.values().any(|s| matches!(s.status, Status::Error)) {
}, Status::Error
"timestamp": chrono::Utc::now().to_rfc3339() } 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 /// Metrics endpoint for monitoring

View File

@@ -6,26 +6,18 @@ export interface HealthResponse {
timestamp: string; timestamp: string;
} }
export type Status = "Disabled" | "Connected" | "Active" | "Healthy" | "Error";
export interface ServiceInfo {
name: string;
status: Status;
}
export interface StatusResponse { export interface StatusResponse {
status: string; status: Status;
version: string; version: string;
bot: {
status: string;
uptime: string;
};
cache: {
status: string;
courses: string;
subjects: string;
};
banner_api: {
status: string;
};
git: {
commit: string; commit: string;
short: string; services: Record<string, ServiceInfo>;
};
timestamp: string;
} }
export interface MetricsResponse { export interface MetricsResponse {

View File

@@ -1,17 +1,17 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { useState, useEffect } from "react"; 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 { Card, Flex, Text, Tooltip } from "@radix-ui/themes";
import { import {
CheckCircle, CheckCircle,
AlertCircle,
XCircle, XCircle,
Clock, Clock,
Bot, Bot,
Database,
Globe, Globe,
Hourglass, Hourglass,
Activity, Activity,
MessageCircle,
Circle,
} from "lucide-react"; } from "lucide-react";
import TimeAgo from "react-timeago"; import TimeAgo from "react-timeago";
import "../App.css"; import "../App.css";
@@ -34,10 +34,14 @@ const BORDER_STYLES = {
borderTop: "1px solid #e2e8f0", borderTop: "1px solid #e2e8f0",
} as const; } as const;
// Types // Service icon mapping
type HealthStatus = "healthy" | "warning" | "error" | "unknown"; const SERVICE_ICONS: Record<string, typeof Bot> = {
type ServiceStatus = "running" | "connected" | "disconnected" | "error"; bot: Bot,
banner: Globe,
discord: MessageCircle,
};
// Types
interface ResponseTiming { interface ResponseTiming {
health: number | null; health: number | null;
status: number | null; status: number | null;
@@ -50,71 +54,72 @@ interface StatusIcon {
interface Service { interface Service {
name: string; name: string;
status: ServiceStatus; status: Status;
icon: typeof Bot; icon: typeof Bot;
} }
// Helper functions // Helper functions
const getStatusIcon = (status: string): StatusIcon => { const getStatusIcon = (status: Status): StatusIcon => {
const statusMap: Record<string, StatusIcon> = { const statusMap: Record<Status, StatusIcon> = {
healthy: { icon: CheckCircle, color: "green" }, Active: { icon: CheckCircle, color: "green" },
running: { icon: CheckCircle, color: "green" }, Connected: { icon: CheckCircle, color: "green" },
connected: { icon: CheckCircle, color: "green" }, Healthy: { icon: CheckCircle, color: "green" },
warning: { icon: AlertCircle, color: "orange" }, Disabled: { icon: Circle, color: "gray" },
error: { icon: XCircle, color: "red" }, Error: { icon: XCircle, color: "red" },
disconnected: { icon: XCircle, color: "red" },
}; };
return statusMap[status] || { icon: XCircle, color: "red" }; return statusMap[status];
}; };
const getOverallHealth = ( const getOverallHealth = (
status: StatusResponse | null, status: StatusResponse | null,
error: string | null error: string | null
): HealthStatus => { ): Status => {
if (error) return "error"; if (error) return "Error";
if (!status) return "unknown"; if (!status) return "Error";
const allHealthy = return status.status;
status.bot.status === "running" &&
status.cache.status === "connected" &&
status.banner_api.status === "connected";
return allHealthy ? "healthy" : "warning";
}; };
const getServices = (status: StatusResponse | null): Service[] => { const getServices = (status: StatusResponse | null): Service[] => {
if (!status) return []; if (!status) return [];
return [ return Object.entries(status.services).map(([serviceId, serviceInfo]) => ({
{ name: "Bot", status: status.bot.status as ServiceStatus, icon: Bot }, name: serviceInfo.name,
{ status: serviceInfo.status,
name: "Cache", icon: SERVICE_ICONS[serviceId] || SERVICE_ICONS.default,
status: status.cache.status as ServiceStatus, }));
icon: Database, };
},
{ // Status Component
name: "Banner API", const StatusDisplay = ({ status }: { status: Status }) => {
status: status.banner_api.status as ServiceStatus, const { icon: Icon, color } = getStatusIcon(status);
icon: Globe,
}, return (
]; <Flex align="center" gap="2">
<Text
size="2"
style={{
color: status === "Disabled" ? "#8B949E" : undefined,
opacity: status === "Disabled" ? 0.7 : undefined,
}}
>
{status}
</Text>
<Icon color={color} size={16} />
</Flex>
);
}; };
// Service Status Component // Service Status Component
const ServiceStatus = ({ service }: { service: Service }) => { const ServiceStatus = ({ service }: { service: Service }) => {
const { icon: Icon, color } = getStatusIcon(service.status);
return ( return (
<Flex align="center" justify="between"> <Flex align="center" justify="between">
<Flex align="center" gap="2"> <Flex align="center" gap="2">
<service.icon size={18} /> <service.icon size={18} />
<Text>{service.name}</Text> <Text>{service.name}</Text>
</Flex> </Flex>
<Flex align="center" gap="2"> <StatusDisplay status={service.status} />
<Icon color={color} size={16} />
<Text size="2">{service.status}</Text>
</Flex>
</Flex> </Flex>
); );
}; };
@@ -193,6 +198,7 @@ function App() {
<Activity color={overallColor} size={18} /> <Activity color={overallColor} size={18} />
<Text size="4">System Status</Text> <Text size="4">System Status</Text>
</Flex> </Flex>
<StatusDisplay status={overallHealth} />
</Flex> </Flex>
{/* Individual Services */} {/* Individual Services */}
@@ -234,7 +240,7 @@ function App() {
</Flex> </Flex>
</Card> </Card>
)} )}
{(status?.git?.commit || status?.version) && ( {(status?.commit || status?.version) && (
<Flex <Flex
justify="center" justify="center"
style={{ marginTop: "12px" }} style={{ marginTop: "12px" }}
@@ -251,7 +257,7 @@ function App() {
v{status.version} v{status.version}
</Text> </Text>
)} )}
{status?.version && status?.git?.commit && ( {status?.version && status?.commit && (
<div <div
style={{ style={{
width: "1px", width: "1px",
@@ -261,7 +267,7 @@ function App() {
}} }}
/> />
)} )}
{status?.git?.commit && ( {status?.commit && (
<Text <Text
size="1" size="1"
style={{ style={{
@@ -270,7 +276,7 @@ function App() {
}} }}
> >
<a <a
href={`https://github.com/Xevion/banner/commit/${status.git.commit}`} href={`https://github.com/Xevion/banner/commit/${status.commit}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ style={{