use http::{Response, StatusCode};
use crate::app_state::AppState;
use crate::router::{Body, html_response};
use crate::routes::layout::{Nav, html_escape, wrap_page};
fn action_badge(action: &str) -> String {
let class = match action {
"create" => "badge-create",
"update" => "badge-update",
"delete" => "badge-delete",
"import" => "badge-import",
"export" => "badge-export",
_ => "badge-create",
};
format!(r#"<span class="badge {class}">{action}</span>"#)
}
pub async fn dashboard(
state: AppState,
_parts: http::request::Parts,
_params: Vec<(String, String)>,
) -> Response<Body> {
let doc_count = state.csaf_storage().count_documents().unwrap_or(0);
let advisory_count = state
.csaf_storage()
.list_by_category("csaf_security_advisory")
.map(|v| v.len())
.unwrap_or(0);
let vex_count = state
.csaf_storage()
.list_by_category("csaf_vex")
.map(|v| v.len())
.unwrap_or(0);
let informational_count = state
.csaf_storage()
.list_by_category("csaf_informational_advisory")
.map(|v| v.len())
.unwrap_or(0);
let recent_activity = state
.db_pool()
.with_conn(|conn| csaf_models::audit_log::list(conn, None, 20, 0))
.unwrap_or_else(|_| Vec::new());
let mut activity_rows = String::new();
if recent_activity.is_empty() {
activity_rows.push_str(
r#"<tr><td colspan="4" class="muted" style="text-align:center;">No activity recorded yet.</td></tr>"#,
);
} else {
for entry in &recent_activity {
let details_display = entry.details.as_deref().unwrap_or("-");
activity_rows.push_str(&format!(
"<tr><td>{ts}</td><td>{badge}</td><td><a href=\"/csaf/{tid}\">{tid}</a></td><td>{details}</td></tr>",
ts = html_escape(&entry.timestamp),
badge = action_badge(&entry.action),
tid = html_escape(&entry.tracking_id),
details = html_escape(details_display),
));
}
}
let settings = state.settings();
let content = format!(
r#"<h1>Dashboard</h1>
<div class="card stats">
<div class="stat"><div class="number">{doc_count}</div><div class="label">Total Documents</div></div>
<div class="stat"><div class="number">{advisory_count}</div><div class="label">Security Advisories</div></div>
<div class="stat"><div class="number">{vex_count}</div><div class="label">VEX Documents</div></div>
<div class="stat"><div class="number">{informational_count}</div><div class="label">Informational</div></div>
</div>
<div class="card">
<h2>Configuration</h2>
<p>CSAF Mode: <strong>{csaf_mode}</strong> | Theme: <strong>{theme}</strong> | Naming: <strong>{naming}</strong></p>
</div>
<div class="card">
<h2>Recent Activity</h2>
<table>
<thead><tr><th>Timestamp</th><th>Action</th><th>Tracking ID</th><th>Details</th></tr></thead>
<tbody>{activity_rows}</tbody>
</table>
</div>"#,
csaf_mode = html_escape(&settings.csaf_mode),
theme = html_escape(&settings.theme),
naming = html_escape(&settings.naming_convention),
);
html_response(
StatusCode::OK,
wrap_page("Dashboard", &settings.theme, Nav::Home, &content),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_action_badge_classes() {
assert!(action_badge("create").contains("badge-create"));
assert!(action_badge("delete").contains("badge-delete"));
assert!(action_badge("import").contains("badge-import"));
}
}