mirror of
https://github.com/Xevion/banner.git
synced 2025-12-06 17:14:25 -06:00
refactor: proper implementation of services status, better styling/appearance/logic
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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={{
|
||||
|
||||
Reference in New Issue
Block a user