Files
2026-05-20 13:41:40 -04:00

203 lines
6.1 KiB
Rust

// TODO(frontend): the maud+htmx views currently inline mock data
// instead of going through this `FleetService` trait. Once a view
// reads from the trait, drop this allow. The structs and the mock
// implementation are intentionally complete so the wiring step is
// purely additive on the views side.
#![allow(dead_code)]
pub mod mock;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::Serialize;
#[async_trait]
pub trait FleetService: Send + Sync + 'static {
async fn dashboard_detail(&self) -> anyhow::Result<DashboardDetail>;
async fn list_devices(&self) -> anyhow::Result<Vec<DeviceDetail>>;
async fn get_device(&self, id: &str) -> anyhow::Result<Option<DeviceDetail>>;
async fn list_deployments(&self) -> anyhow::Result<Vec<DeploymentDetail>>;
async fn get_deployment(&self, name: &str) -> anyhow::Result<Option<DeploymentDetail>>;
async fn get_deployment_devices(&self, name: &str) -> anyhow::Result<Vec<DeviceDetail>>;
async fn blacklist_device(&self, id: &str) -> anyhow::Result<DeviceDetail>;
async fn list_alerts(&self) -> anyhow::Result<Vec<Alert>>;
async fn ack_alert(&self, id: &str) -> anyhow::Result<bool>;
async fn get_task_graph(&self, deployment: &str) -> anyhow::Result<TaskGraph>;
async fn filtered_devices(
&self,
status: Option<DeviceStatus>,
deployment: Option<String>,
region: Option<String>,
search: Option<String>,
) -> anyhow::Result<Vec<DeviceDetail>>;
}
// ── Device ─────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize)]
pub struct DeviceDetail {
pub id: String,
pub status: DeviceStatus,
pub last_seen: DateTime<Utc>,
pub minutes_ago: i64,
pub deployment: Option<String>,
pub ip: Option<String>,
pub region: String,
pub model: String,
pub fw: String,
pub tags: Vec<String>,
pub uptime_h: u32,
pub cpu: u8,
pub mem: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DeviceStatus {
Healthy,
Pending,
Stale,
Failing,
Blacklisted,
Unknown,
}
impl DeviceStatus {
pub fn label(self) -> &'static str {
match self {
Self::Healthy => "healthy",
Self::Pending => "pending",
Self::Stale => "stale",
Self::Failing => "failing",
Self::Blacklisted => "blacklisted",
Self::Unknown => "unknown",
}
}
}
// ── Deployment ─────────────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize)]
pub struct DeploymentDetail {
pub name: String,
pub version: String,
pub status: DeploymentStatus,
pub target: u32,
pub healthy: u32,
pub failing: u32,
pub pending: u32,
pub updated_at: String,
pub author: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DeploymentStatus {
Active,
Rolling,
Failing,
Paused,
}
impl DeploymentStatus {
pub fn label(self) -> &'static str {
match self {
Self::Active => "active",
Self::Rolling => "rolling",
Self::Failing => "failing",
Self::Paused => "paused",
}
}
}
// ── Dashboard ──────────────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize)]
pub struct DashboardDetail {
pub devices_total: u32,
pub devices_healthy: u32,
pub devices_pending: u32,
pub devices_failing: u32,
pub devices_stale: u32,
pub devices_blacklisted: u32,
pub devices_unknown: u32,
pub deployments_total: usize,
pub health_pct: u32,
pub health_trend: Vec<f64>,
pub ingest_rate: u32,
pub ingest_trend: Vec<u32>,
pub attention_devices: Vec<DeviceDetail>,
pub activity_feed: Vec<Activity>,
pub top_deployments: Vec<DeploymentDetail>,
pub active_alerts: Vec<Alert>,
pub rolling_count: usize,
pub failing_count: usize,
}
// ── Alert ──────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize)]
pub struct Alert {
pub id: String,
pub severity: AlertSeverity,
pub title: String,
pub deployment: Option<String>,
pub device: Option<String>,
pub at: String,
pub acked: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum AlertSeverity {
Critical,
Warning,
Info,
}
impl AlertSeverity {
pub fn label(self) -> &'static str {
match self {
Self::Critical => "critical",
Self::Warning => "warning",
Self::Info => "info",
}
}
}
// ── Activity ───────────────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize)]
pub struct Activity {
pub who: String,
pub verb: String,
pub target: String,
pub at: String,
}
// ── Task Graph ─────────────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize)]
pub struct TaskGraph {
pub nodes: Vec<TaskNode>,
pub edges: Vec<(String, String)>,
pub positions: std::collections::HashMap<String, (usize, usize)>,
}
#[derive(Debug, Clone, Serialize)]
pub struct TaskNode {
pub id: String,
pub label: String,
pub status: TaskStatus,
pub duration: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum TaskStatus {
Done,
Running,
Pending,
Failed,
}