use crate::dashboard::routes::helpers::detect_project_root_for_dashboard;
pub(super) fn get_route(
path: &str,
query_str: &str,
) -> Option<(&'static str, &'static str, String)> {
match path {
"/api/heatmap" => Some(heatmap()),
"/api/graph" => Some(graph()),
"/api/graph/enrich" => Some(enrich()),
"/api/graph/stats" => Some(stats()),
"/api/graph-files" => Some(graph_files()),
"/api/routes" => Some(routes(query_str)),
_ => None,
}
}
fn heatmap() -> (&'static str, &'static str, String) {
let project_root = detect_project_root_for_dashboard();
let Some(open) = crate::core::graph_provider::open_or_build(&project_root) else {
return ("200 OK", "application/json", "[]".to_string());
};
let entries = build_heatmap_json(&open.provider);
("200 OK", "application/json", entries)
}
fn graph() -> (&'static str, &'static str, String) {
let root = detect_project_root_for_dashboard();
let Some(open) = crate::core::graph_provider::open_or_build(&root) else {
return (
"200 OK",
"application/json",
"{\"error\":\"no graph\"}".to_string(),
);
};
let gp = &open.provider;
let all_edges = gp.edges();
let mut edge_stats: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
for edge in &all_edges {
*edge_stats.entry(edge.kind.as_str()).or_default() += 1;
}
let connected: std::collections::HashSet<&str> = all_edges
.iter()
.flat_map(|e| [e.from.as_str(), e.to.as_str()])
.collect();
let file_count = gp.file_count();
let isolated_count = file_count - connected.len().min(file_count);
let files: Vec<serde_json::Value> = gp
.file_paths()
.iter()
.filter_map(|p| {
gp.get_file_entry(p).map(|f| {
serde_json::json!({
"path": f.path,
"language": f.language,
"token_count": f.token_count,
"line_count": f.line_count,
"exports": f.exports,
"summary": f.summary,
})
})
})
.collect();
let edges: Vec<serde_json::Value> = all_edges
.iter()
.map(|e| {
serde_json::json!({
"from": e.from,
"to": e.to,
"kind": e.kind,
"weight": e.weight,
})
})
.collect();
let val = serde_json::json!({
"project_root": super::project_basename(&root),
"project_root_full": root,
"files": files,
"edges": edges,
"edge_stats": edge_stats,
"isolated_node_count": isolated_count,
"orphan_rate": if file_count > 0 {
(isolated_count as f64 / file_count as f64 * 100.0).round() / 100.0
} else {
0.0
},
});
let json = serde_json::to_string(&val)
.unwrap_or_else(|_| "{\"error\":\"failed to serialize\"}".to_string());
("200 OK", "application/json", json)
}
fn enrich() -> (&'static str, &'static str, String) {
let root = detect_project_root_for_dashboard();
let project_path = std::path::Path::new(&root);
let result = match crate::core::property_graph::CodeGraph::open(&root) {
Ok(graph) => match crate::core::graph_enricher::enrich_graph(&graph, project_path, 500) {
Ok(stats) => {
let nc = graph.node_count().unwrap_or(0);
let ec = graph.edge_count().unwrap_or(0);
serde_json::json!({
"commits_indexed": stats.commits_indexed,
"tests_indexed": stats.tests_indexed,
"knowledge_indexed": stats.knowledge_indexed,
"edges_created": stats.edges_created,
"total_nodes": nc,
"total_edges": ec,
})
}
Err(e) => {
tracing::warn!("graph enrich error: {e}");
serde_json::json!({"error": "enrichment_failed"})
}
},
Err(e) => {
tracing::warn!("graph open error: {e}");
serde_json::json!({"error": "graph_unavailable"})
}
};
let json = serde_json::to_string(&result).unwrap_or_else(|_| "{}".to_string());
("200 OK", "application/json", json)
}
fn stats() -> (&'static str, &'static str, String) {
let root = detect_project_root_for_dashboard();
let result = if let Some(open) = crate::core::graph_provider::open_best_effort(&root) {
let nc = open.provider.node_count().unwrap_or(0);
let ec = open.provider.edge_count().unwrap_or(0);
match open.source {
crate::core::graph_provider::GraphProviderSource::PropertyGraph => {
serde_json::json!({
"source": "property_graph",
"node_count": nc,
"edge_count": ec,
})
}
crate::core::graph_provider::GraphProviderSource::GraphIndex => {
serde_json::json!({
"source": "graph_index",
"node_count": nc,
"edge_count": ec,
})
}
}
} else {
serde_json::json!({
"source": "none",
"node_count": 0,
"edge_count": 0,
})
};
let json = serde_json::to_string(&result).unwrap_or_else(|_| "{}".to_string());
("200 OK", "application/json", json)
}
fn graph_files() -> (&'static str, &'static str, String) {
let root = detect_project_root_for_dashboard();
let Some(open) = crate::core::graph_provider::open_or_build(&root) else {
return ("200 OK", "application/json", "{\"files\":[]}".to_string());
};
let gp = &open.provider;
let mut files: Vec<serde_json::Value> = gp
.file_paths()
.iter()
.filter_map(|p| {
gp.get_file_entry(p).map(|f| {
serde_json::json!({
"path": f.path,
"language": f.language,
"token_count": f.token_count,
"line_count": f.line_count,
})
})
})
.collect();
files.sort_by(|a, b| {
b["token_count"]
.as_u64()
.unwrap_or(0)
.cmp(&a["token_count"].as_u64().unwrap_or(0))
});
files.truncate(500);
let json = serde_json::json!({ "files": files, "project_root_full": root });
let out = serde_json::to_string(&json).unwrap_or_else(|_| "{\"files\":[]}".to_string());
("200 OK", "application/json", out)
}
fn routes(_query_str: &str) -> (&'static str, &'static str, String) {
let root = detect_project_root_for_dashboard();
let Some(open) = crate::core::graph_provider::open_or_build(&root) else {
return ("200 OK", "application/json", "{\"routes\":[]}".to_string());
};
let gp = &open.provider;
let file_paths = gp.file_paths();
let files_map: std::collections::HashMap<String, crate::core::graph_index::FileEntry> =
file_paths
.iter()
.filter_map(|p| {
gp.get_file_entry(p).map(|f| {
(
p.clone(),
crate::core::graph_index::FileEntry {
path: f.path,
hash: f.hash,
language: f.language,
line_count: f.line_count,
token_count: f.token_count,
exports: f.exports,
summary: f.summary,
},
)
})
})
.collect();
let routes = crate::core::route_extractor::extract_routes_from_project(&root, &files_map);
let route_candidate_count = file_paths
.iter()
.filter(|p| {
std::path::Path::new(p.as_str())
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| {
matches!(e, "js" | "ts" | "py" | "rs" | "java" | "rb" | "go" | "kt")
})
})
.count();
let payload = serde_json::json!({
"routes": routes,
"indexed_file_count": file_paths.len(),
"route_candidate_count": route_candidate_count,
});
let json = serde_json::to_string(&payload).unwrap_or_else(|_| "{\"routes\":[]}".to_string());
("200 OK", "application/json", json)
}
fn build_heatmap_json(gp: &crate::core::graph_provider::GraphProvider) -> String {
let all_edges = gp.edges();
let mut connection_counts: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for edge in &all_edges {
*connection_counts.entry(edge.from.clone()).or_default() += 1;
*connection_counts.entry(edge.to.clone()).or_default() += 1;
}
let paths = gp.file_paths();
let mut max_tokens = 1usize;
for path in &paths {
if let Some(entry) = gp.get_file_entry(path) {
max_tokens = max_tokens.max(entry.token_count);
}
}
let max_tokens = max_tokens as f64;
let max_connections = connection_counts.values().max().copied().unwrap_or(1) as f64;
let mut entries: Vec<serde_json::Value> = paths
.iter()
.filter_map(|p| {
gp.get_file_entry(p).map(|f| {
let connections = connection_counts.get(&f.path).copied().unwrap_or(0);
let token_norm = f.token_count as f64 / max_tokens;
let conn_norm = connections as f64 / max_connections;
let heat = token_norm * 0.4 + conn_norm * 0.6;
serde_json::json!({
"path": f.path,
"tokens": f.token_count,
"connections": connections,
"language": f.language,
"heat": (heat * 100.0).round() / 100.0,
})
})
})
.collect();
entries.sort_by(|a, b| {
b["heat"]
.as_f64()
.unwrap_or(0.0)
.partial_cmp(&a["heat"].as_f64().unwrap_or(0.0))
.unwrap_or(std::cmp::Ordering::Equal)
});
serde_json::to_string(&entries).unwrap_or_else(|_| "[]".to_string())
}