Add counting of detached nodes

This commit is contained in:
Juno Takano 2026-01-13 15:32:36 -03:00
commit 41a5994bbd
7 changed files with 109 additions and 18 deletions

View file

@ -30,6 +30,13 @@ pub struct Graph {
pub lowercase_keymap: HashMap<String, String>,
#[serde(default)]
pub meta: Meta,
#[serde(default)]
pub stats: Stats,
}
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
pub struct Stats {
pub detached: HashMap<String, u32>,
}
#[derive(Clone, Default, Debug)]
@ -160,8 +167,11 @@ impl Graph {
}
// Flag detached edges
if !in_nodes.contains_key(&edge.to) {
if in_nodes.contains_key(&edge.to) {
new_edge.detached = false;
} else {
new_edge.detached = true;
self.increment_detached(&key);
}
if let Some(e) = new_edges.get_mut(&connection_key) {
@ -171,14 +181,19 @@ impl Graph {
// Create connections for each link
for link in &node.links {
let detached = !in_nodes.contains_key(link);
new_edges.insert(
link.clone(),
Edge {
from: key.clone(),
to: link.clone(),
detached: !in_nodes.clone().contains_key(link),
detached,
},
);
if detached {
self.increment_detached(&key);
}
}
// Populate empty titles with IDs
@ -269,11 +284,35 @@ impl Graph {
},
);
}
} else {
if let Some(destination) = anchor.destination()
&& !anchor.external()
{
self.stats
.detached
.entry(
destination
.trim_start_matches("/node/")
.to_string(),
)
.and_modify(|count| {
*count = count.saturating_add(1)
})
.or_insert(1);
}
}
}
}
}
fn increment_detached(&mut self, node_id: &str) {
self.stats
.detached
.entry(node_id.to_string())
.and_modify(|count| *count = count.saturating_add(1))
.or_insert(1);
}
pub fn map_lowercase_keys(&mut self) {
for key in self.nodes.keys() {
self.lowercase_keymap

View file

@ -23,6 +23,15 @@ pub struct Node {
#[serde(skip_serializing_if = "Option::is_none")]
pub connections: Option<HashMap<String, Edge>>,
#[serde(default)]
pub stats: Stats,
}
#[derive(Serialize, Deserialize, Clone, Default, Eq, PartialEq, Debug)]
pub struct Stats {
pub outgoing: u32,
pub incoming: u32,
}
impl Node {
@ -39,6 +48,7 @@ impl Node {
redirect: String::default(),
hidden: false,
summary: String::default(),
stats: Stats::default(),
}
}
}

View file

@ -18,8 +18,13 @@ pub fn new(graph: &Graph) -> Router {
get(|| handlers::navigation::page("index.html"))
.post(handlers::navigation::search),
)
.route("/redirect", get(handlers::navigation::redirect))
.route(
"/node/{node_id}",
get(handlers::graph::node).post(handlers::graph::node),
)
.route("/data", get(handlers::navigation::data))
.route("/search", get(handlers::navigation::search))
.route("/redirect", get(handlers::navigation::redirect))
.route(
"/static/style.css",
get(|| handlers::fixed::file("./static/style.css", "text/css")),
@ -30,10 +35,6 @@ pub fn new(graph: &Graph) -> Router {
handlers::fixed::file("./static/favicon.svg", "image/svg+xml")
}),
)
.route(
"/node/{node_id}",
get(handlers::graph::node).post(handlers::graph::node),
)
.fallback(handlers::error::not_found);
if graph.meta.config.about {
@ -47,8 +48,6 @@ pub fn new(graph: &Graph) -> Router {
}
if graph.meta.config.raw {
router = router
.route("/data", get(|| handlers::navigation::page("data.html")));
if graph.meta.config.raw_json {
router = router.route(
"/graph/json",

View file

@ -14,16 +14,27 @@ use crate::{
pub async fn page(template: &str) -> Response<Body> {
let mut context = tera::Context::default();
let graph = Graph::load();
let root_node = graph.get_root().unwrap_or_default();
let nodes: Vec<Node> = graph.nodes.into_values().collect();
context.insert("nodes", &nodes);
context.insert("root_node", &root_node);
context.insert("config", &graph.meta.config);
context.insert("graph", &graph);
handlers::template::by_filename(template, &context, 500, None, false)
}
pub async fn data() -> Response<Body> {
let mut context = tera::Context::default();
let graph = Graph::load();
let mut detached_pairs: Vec<(String, u32)> =
graph.stats.detached.clone().into_iter().collect();
detached_pairs.sort_by(|a, b| b.1.cmp(&a.1));
context.insert("graph", &graph);
context.insert("detached_count", &graph.stats.detached.len());
context.insert("detached_pairs", &detached_pairs);
handlers::template::by_filename("data.html", &context, 500, None, false)
}
pub async fn search(Form(query): Form<Query>) -> Redirect {
Redirect::permanent(format!("/node/{}", query.node).as_str())
}