mirror of
https://github.com/Xevion/banner.git
synced 2025-12-06 19:14:23 -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,
|
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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
Reference in New Issue
Block a user