/// Application version from Cargo.toml const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Main layout wrapper with header and sidebar pub fn layout(title: &str, content: &str, active_page: Option<&str>) -> String { format!( r##" {} - Nora
{}
{}
{}
"##, html_escape(title), sidebar(active_page), header(), content ) } /// Dark theme layout wrapper for dashboard pub fn layout_dark( title: &str, content: &str, active_page: Option<&str>, extra_scripts: &str, ) -> String { format!( r##" {} - Nora
{}
{}
{}
{} "##, html_escape(title), sidebar_dark(active_page), header_dark(), content, extra_scripts ) } /// Dark theme sidebar fn sidebar_dark(active_page: Option<&str>) -> String { let active = active_page.unwrap_or(""); let docker_icon = r#""#; let maven_icon = r#""#; let npm_icon = r#""#; let cargo_icon = r#""#; let pypi_icon = r#""#; let nav_items = [ ( "dashboard", "/ui/", "Dashboard", r#""#, true, ), ("docker", "/ui/docker", "Docker", docker_icon, false), ("maven", "/ui/maven", "Maven", maven_icon, false), ("npm", "/ui/npm", "npm", npm_icon, false), ("cargo", "/ui/cargo", "Cargo", cargo_icon, false), ("pypi", "/ui/pypi", "PyPI", pypi_icon, false), ]; let nav_html: String = nav_items.iter().map(|(id, href, label, icon_path, is_stroke)| { let is_active = active == *id; let active_class = if is_active { "bg-slate-700 text-white" } else { "text-slate-300 hover:bg-slate-700 hover:text-white" }; let (fill_attr, stroke_attr) = if *is_stroke { ("none", r#" stroke="currentColor""#) } else { ("currentColor", "") }; format!(r##" {} {} "##, href, active_class, fill_attr, stroke_attr, icon_path, label) }).collect(); format!( r#" "#, super::logo::LOGO_BASE64, nav_html, VERSION ) } /// Dark theme header fn header_dark() -> String { r##"
NRA
"##.to_string() } /// Render global stats row (5-column grid) pub fn render_global_stats( downloads: u64, uploads: u64, artifacts: u64, cache_hit_percent: f64, storage_bytes: u64, ) -> String { format!( r##"
Downloads
{}
Uploads
{}
Artifacts
{}
Cache Hit
{:.1}%
Storage
{}
"##, downloads, uploads, artifacts, cache_hit_percent, format_size(storage_bytes) ) } /// Render registry card with extended metrics pub fn render_registry_card( name: &str, icon_path: &str, artifact_count: usize, downloads: u64, uploads: u64, size_bytes: u64, href: &str, ) -> String { format!( r##"
{} ACTIVE
{}
Artifacts
{}
Size
{}
Downloads
{}
Uploads
{}
"##, href, name.to_lowercase(), icon_path, name, artifact_count, format_size(size_bytes), downloads, uploads ) } /// Render mount points table pub fn render_mount_points_table(mount_points: &[(String, String, Option)]) -> String { let rows: String = mount_points .iter() .map(|(registry, mount_path, proxy)| { let proxy_display = proxy.as_deref().unwrap_or("-"); format!( r##" {} {} {} "##, registry, mount_path, proxy_display ) }) .collect(); format!( r##"

Mount Points

{}
Registry Mount Path Proxy Upstream
"##, rows ) } /// Render a single activity log row pub fn render_activity_row( timestamp: &str, action: &str, artifact: &str, registry: &str, source: &str, ) -> String { let action_color = match action { "PULL" => "text-blue-400", "PUSH" => "text-green-400", "CACHE" => "text-yellow-400", "PROXY" => "text-purple-400", _ => "text-slate-400", }; format!( r##" {} {} {} {} {} "##, timestamp, action_color, action, html_escape(artifact), registry, source ) } /// Render the activity log container pub fn render_activity_log(rows: &str) -> String { format!( r##"

Recent Activity

{}
Time Action Artifact Registry Source
"##, rows ) } /// Render the polling script for auto-refresh pub fn render_polling_script() -> String { r##" "##.to_string() } /// Sidebar navigation component fn sidebar(active_page: Option<&str>) -> String { let active = active_page.unwrap_or(""); // SVG icon paths for registries (Simple Icons style) let docker_icon = r#""#; let maven_icon = r#""#; let npm_icon = r#""#; let cargo_icon = r#""#; let pypi_icon = r#""#; let nav_items = [ ( "dashboard", "/ui/", "Dashboard", r#""#, true, // stroke icon ), ("docker", "/ui/docker", "Docker", docker_icon, false), ("maven", "/ui/maven", "Maven", maven_icon, false), ("npm", "/ui/npm", "npm", npm_icon, false), ("cargo", "/ui/cargo", "Cargo", cargo_icon, false), ("pypi", "/ui/pypi", "PyPI", pypi_icon, false), ]; let nav_html: String = nav_items.iter().map(|(id, href, label, icon_path, is_stroke)| { let is_active = active == *id; let active_class = if is_active { "bg-slate-700 text-white" } else { "text-slate-300 hover:bg-slate-700 hover:text-white" }; // SVG attributes differ for stroke vs fill icons let (fill_attr, stroke_attr) = if *is_stroke { ("none", r#" stroke="currentColor""#) } else { ("currentColor", "") }; format!(r##" {} {} "##, href, active_class, fill_attr, stroke_attr, icon_path, label) }).collect(); format!( r#" "#, super::logo::LOGO_BASE64, nav_html, VERSION ) } /// Header component fn header() -> String { r##"
NRA
"##.to_string() } /// SVG icon definitions for registries (exported for use in templates) pub mod icons { pub const DOCKER: &str = r#""#; pub const MAVEN: &str = r#""#; pub const NPM: &str = r#""#; pub const CARGO: &str = r#""#; pub const PYPI: &str = r#""#; } /// Stat card for dashboard with SVG icon (used in light theme pages) #[allow(dead_code)] pub fn stat_card(name: &str, icon_path: &str, count: usize, href: &str, unit: &str) -> String { format!( r##"
{} ACTIVE
{}
{}
{}
"##, href, icon_path, name, count, unit ) } /// Format file size in human-readable format pub fn format_size(bytes: u64) -> String { const KB: u64 = 1024; const MB: u64 = KB * 1024; const GB: u64 = MB * 1024; if bytes >= GB { format!("{:.1} GB", bytes as f64 / GB as f64) } else if bytes >= MB { format!("{:.1} MB", bytes as f64 / MB as f64) } else if bytes >= KB { format!("{:.1} KB", bytes as f64 / KB as f64) } else { format!("{} B", bytes) } } /// Escape HTML special characters pub fn html_escape(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } /// Format Unix timestamp as relative time pub fn format_timestamp(ts: u64) -> String { if ts == 0 { return "N/A".to_string(); } let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); if now < ts { return "just now".to_string(); } let diff = now - ts; if diff < 60 { "just now".to_string() } else if diff < 3600 { let mins = diff / 60; format!("{} min{} ago", mins, if mins == 1 { "" } else { "s" }) } else if diff < 86400 { let hours = diff / 3600; format!("{} hour{} ago", hours, if hours == 1 { "" } else { "s" }) } else if diff < 604800 { let days = diff / 86400; format!("{} day{} ago", days, if days == 1 { "" } else { "s" }) } else if diff < 2592000 { let weeks = diff / 604800; format!("{} week{} ago", weeks, if weeks == 1 { "" } else { "s" }) } else { let months = diff / 2592000; format!("{} month{} ago", months, if months == 1 { "" } else { "s" }) } }