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,
};
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<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
async fn status(State(_state): State<BannerState>) -> Json<Value> {
// 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<BannerState>) -> Json<StatusResponse> {
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

View File

@@ -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;
services: Record<string, ServiceInfo>;
}
export interface MetricsResponse {

View File

@@ -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<string, typeof Bot> = {
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<string, StatusIcon> = {
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<Status, StatusIcon> = {
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 (
<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
const ServiceStatus = ({ service }: { service: Service }) => {
const { icon: Icon, color } = getStatusIcon(service.status);
return (
<Flex align="center" justify="between">
<Flex align="center" gap="2">
<service.icon size={18} />
<Text>{service.name}</Text>
</Flex>
<Flex align="center" gap="2">
<Icon color={color} size={16} />
<Text size="2">{service.status}</Text>
</Flex>
<StatusDisplay status={service.status} />
</Flex>
);
};
@@ -193,6 +198,7 @@ function App() {
<Activity color={overallColor} size={18} />
<Text size="4">System Status</Text>
</Flex>
<StatusDisplay status={overallHealth} />
</Flex>
{/* Individual Services */}
@@ -234,7 +240,7 @@ function App() {
</Flex>
</Card>
)}
{(status?.git?.commit || status?.version) && (
{(status?.commit || status?.version) && (
<Flex
justify="center"
style={{ marginTop: "12px" }}
@@ -251,7 +257,7 @@ function App() {
v{status.version}
</Text>
)}
{status?.version && status?.git?.commit && (
{status?.version && status?.commit && (
<div
style={{
width: "1px",
@@ -261,7 +267,7 @@ function App() {
}}
/>
)}
{status?.git?.commit && (
{status?.commit && (
<Text
size="1"
style={{
@@ -270,7 +276,7 @@ function App() {
}}
>
<a
href={`https://github.com/Xevion/banner/commit/${status.git.commit}`}
href={`https://github.com/Xevion/banner/commit/${status.commit}`}
target="_blank"
rel="noopener noreferrer"
style={{