use std::sync::Arc;
use axum::extract::{Path, State};
use maud::{html, Markup};
use crate::web::templates::{
format_bytes, layout, m_breadcrumb, m_card, m_empty, m_header, m_stat, m_table_header,
};
use crate::web::AdminContext;
use crate::web::NavItem;
pub async fn list_view(State(ctx): State<Arc<AdminContext>>) -> Markup {
let Some(checkpoint) = ctx.checkpoint.as_ref() else {
return layout(
"Checkpoints",
NavItem::Checkpoint,
m_empty(
"Checkpoint Manager Not Configured",
"Enable checkpoint management to view backups.",
),
);
};
let checkpoints = checkpoint.list(Some(50));
let content = match checkpoints {
Ok(list) => {
let count = list.len();
html! {
(m_header("CHECKPOINTS", Some(&format!("{count} checkpoints"))))
@if list.is_empty() {
(m_empty("No Checkpoints", "No checkpoints have been created yet."))
} @else {
div class="m-card" {
div class="m-card-content overflow-x-auto" {
table class="m-table w-full" {
(m_table_header(&["NAME", "SIZE", "TRIGGER", "CREATED"]))
tbody {
@for cp in &list {
tr {
td {
a href=(format!("/checkpoint/{}", cp.id))
class="text-white hover:underline" {
(cp.name)
}
}
td class="text-neutral-400 font-mono" {
(format_bytes(cp.size))
}
td class="text-neutral-400" {
@match &cp.trigger {
Some(t) => (t),
None => "Manual",
}
}
td class="text-neutral-400 font-mono" {
(format_checkpoint_time(cp.created_at))
}
}
}
}
}
}
}
}
div class="mt-4 flex gap-4" {
a href="/checkpoint/config" class="m-btn inline-block" { "VIEW CONFIG" }
}
}
},
Err(e) => {
html! {
(m_header("CHECKPOINTS", None))
(m_empty("Error Loading Checkpoints", &e.to_string()))
}
},
};
layout("Checkpoints", NavItem::Checkpoint, content)
}
pub async fn detail_view(State(ctx): State<Arc<AdminContext>>, Path(id): Path<String>) -> Markup {
let Some(checkpoint) = ctx.checkpoint.as_ref() else {
return layout(
"Checkpoint",
NavItem::Checkpoint,
m_empty(
"Checkpoint Manager Not Configured",
"Enable checkpoint management to view backups.",
),
);
};
let checkpoints = checkpoint.list(Some(100));
let found = checkpoints
.ok()
.and_then(|list| list.into_iter().find(|cp| cp.id == id));
let content = match found {
Some(cp) => {
html! {
(m_breadcrumb(&[
("/checkpoint", "CHECKPOINTS"),
("", &cp.name),
]))
(m_header(&cp.name, Some(&format!("Checkpoint {}", cp.id))))
div class="grid grid-cols-1 lg:grid-cols-2 gap-6" {
(m_card("DETAILS", html! {
dl class="space-y-3" {
div class="flex justify-between" {
dt class="text-neutral-400" { "ID" }
dd class="text-white font-mono text-sm" { (cp.id) }
}
div class="flex justify-between" {
dt class="text-neutral-400" { "Name" }
dd class="text-white" { (cp.name) }
}
div class="flex justify-between" {
dt class="text-neutral-400" { "Size" }
dd class="text-white font-mono" { (format_bytes(cp.size)) }
}
div class="flex justify-between" {
dt class="text-neutral-400" { "Trigger" }
dd class="text-white" {
@match &cp.trigger {
Some(t) => (t),
None => "Manual",
}
}
}
div class="flex justify-between" {
dt class="text-neutral-400" { "Created" }
dd class="text-white font-mono" {
(format_checkpoint_time(cp.created_at))
}
}
}
}))
(m_card("RESTORE", html! {
div class="text-neutral-400 text-sm" {
p class="mb-2" {
"To restore this checkpoint, use the CLI:"
}
code class="block bg-neutral-800 p-3 rounded text-white font-mono text-sm" {
"ROLLBACK TO " (cp.name)
}
}
}))
}
}
},
None => {
html! {
(m_breadcrumb(&[
("/checkpoint", "CHECKPOINTS"),
("", &id),
]))
(m_empty("Checkpoint Not Found", &format!("No checkpoint with ID '{id}'")))
}
},
};
layout("Checkpoint Detail", NavItem::Checkpoint, content)
}
pub async fn config_view(State(ctx): State<Arc<AdminContext>>) -> Markup {
let Some(checkpoint) = ctx.checkpoint.as_ref() else {
return layout(
"Checkpoint Config",
NavItem::Checkpoint,
m_empty(
"Checkpoint Manager Not Configured",
"Enable checkpoint management to view configuration.",
),
);
};
let config = checkpoint.config();
let content = html! {
(m_breadcrumb(&[
("/checkpoint", "CHECKPOINTS"),
("", "CONFIG"),
]))
(m_header("CHECKPOINT CONFIGURATION", None))
div class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-8 stagger-container" {
(m_stat(
"AUTO-CHECKPOINT",
if config.auto_checkpoint { "ON" } else { "OFF" },
"automatic backups",
"checkpoint",
))
(m_stat(
"CONFIRM",
if config.interactive_confirm { "ON" } else { "OFF" },
"interactive confirm",
"checkpoint",
))
(m_stat(
"MAX RETAINED",
&config.max_checkpoints.to_string(),
"checkpoint limit",
"checkpoint",
))
}
};
layout("Checkpoint Config", NavItem::Checkpoint, content)
}
fn format_checkpoint_time(timestamp: u64) -> String {
if timestamp == 0 {
return "--".to_string();
}
let days = timestamp / 86400;
let hours = (timestamp % 86400) / 3600;
if days > 0 {
format!("{days}d {hours}h")
} else {
let mins = (timestamp % 3600) / 60;
format!("{hours}h {mins}m")
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::extract::State;
use graph_engine::GraphEngine;
use relational_engine::RelationalEngine;
use tensor_checkpoint::{CheckpointConfig, CheckpointManager, FileCheckpointStore};
use vector_engine::VectorEngine;
fn create_test_context() -> Arc<AdminContext> {
Arc::new(AdminContext::new(
Arc::new(RelationalEngine::new()),
Arc::new(VectorEngine::new()),
Arc::new(GraphEngine::new()),
))
}
fn create_test_context_with_checkpoint() -> (Arc<AdminContext>, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let store = Arc::new(FileCheckpointStore::new(dir.path()).unwrap());
let mgr = CheckpointManager::new(store, CheckpointConfig::default());
let mut ctx = AdminContext::new(
Arc::new(RelationalEngine::new()),
Arc::new(VectorEngine::new()),
Arc::new(GraphEngine::new()),
);
ctx.checkpoint = Some(Arc::new(mgr));
(Arc::new(ctx), dir)
}
#[tokio::test]
async fn test_list_view_no_checkpoint() {
let ctx = create_test_context();
let result = list_view(State(ctx)).await;
let html = result.into_string();
assert!(html.contains("Not Configured"));
}
#[tokio::test]
async fn test_list_view_empty() {
let (ctx, _dir) = create_test_context_with_checkpoint();
let result = list_view(State(ctx)).await;
let html = result.into_string();
assert!(html.contains("CHECKPOINTS"));
}
#[tokio::test]
async fn test_detail_view_no_checkpoint() {
let ctx = create_test_context();
let result = detail_view(State(ctx), Path("abc".to_string())).await;
let html = result.into_string();
assert!(html.contains("Not Configured"));
}
#[tokio::test]
async fn test_detail_view_not_found() {
let (ctx, _dir) = create_test_context_with_checkpoint();
let result = detail_view(State(ctx), Path("nonexistent".to_string())).await;
let html = result.into_string();
assert!(html.contains("Not Found"));
}
#[tokio::test]
async fn test_config_view_no_checkpoint() {
let ctx = create_test_context();
let result = config_view(State(ctx)).await;
let html = result.into_string();
assert!(html.contains("Not Configured"));
}
#[tokio::test]
async fn test_config_view_with_checkpoint() {
let (ctx, _dir) = create_test_context_with_checkpoint();
let result = config_view(State(ctx)).await;
let html = result.into_string();
assert!(html.contains("CHECKPOINT CONFIGURATION"));
assert!(html.contains("AUTO-CHECKPOINT"));
assert!(html.contains("CONFIRM"));
assert!(html.contains("MAX RETAINED"));
}
fn create_test_context_with_data() -> (Arc<AdminContext>, String, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let file_store = Arc::new(FileCheckpointStore::new(dir.path()).unwrap());
let tensor_store = tensor_store::TensorStore::new();
let mgr = CheckpointManager::new(file_store, CheckpointConfig::default());
let cp_id = mgr
.create(Some("test_backup"), &tensor_store)
.expect("create checkpoint");
let mut ctx = AdminContext::new(
Arc::new(RelationalEngine::new()),
Arc::new(VectorEngine::new()),
Arc::new(GraphEngine::new()),
);
ctx.checkpoint = Some(Arc::new(mgr));
(Arc::new(ctx), cp_id, dir)
}
#[tokio::test]
async fn test_list_view_with_data() {
let (ctx, _id, _dir) = create_test_context_with_data();
let result = list_view(State(ctx)).await;
let html = result.into_string();
assert!(html.contains("test_backup"));
assert!(html.contains("Manual"));
}
#[tokio::test]
async fn test_detail_view_with_data() {
let (ctx, id, _dir) = create_test_context_with_data();
let result = detail_view(State(ctx), Path(id)).await;
let html = result.into_string();
assert!(html.contains("test_backup"));
assert!(html.contains("DETAILS"));
assert!(html.contains("RESTORE"));
assert!(html.contains("ROLLBACK TO"));
}
#[test]
fn test_format_checkpoint_time_zero() {
assert_eq!(format_checkpoint_time(0), "--");
}
#[test]
fn test_format_checkpoint_time_hours() {
assert_eq!(format_checkpoint_time(7200), "2h 0m");
}
#[test]
fn test_format_checkpoint_time_days() {
assert_eq!(format_checkpoint_time(172_800), "2d 0h");
}
}