From 2481f957b3077bb4a69d26e62909354f6da2da2c Mon Sep 17 00:00:00 2001 From: Anton Liashkevich Date: Tue, 23 Jun 2026 13:08:34 -0400 Subject: [PATCH 1/2] server: scope sessions and nodes APIs to a stack /api/sessions and /api/nodes were returning global data across all stacks. Now both require a {stack} path param, matching the pattern established by /api/services, /api/graph, and /api/chains. The teardown endpoint moves from DELETE /api/sessions/{id} to DELETE /api/sessions/{stack}/{id} accordingly. UI updated across Dashboard, Sessions, Nodes, Layout, and TopologyContext to pass the active stack from StackContext. Pool data remains global as it has no per-stack meaning. --- members/nullnet-server/src/http_server/mod.rs | 6 ++-- .../nullnet-server/src/http_server/nodes.rs | 9 +++-- .../src/http_server/sessions.rs | 36 ++++++++++++------- .../ui/src/components/Layout.tsx | 2 +- .../components/topology/TopologyContext.tsx | 2 +- .../nullnet-server/ui/src/pages/Dashboard.tsx | 4 +-- members/nullnet-server/ui/src/pages/Nodes.tsx | 10 ++---- .../nullnet-server/ui/src/pages/Sessions.tsx | 6 ++-- 8 files changed, 44 insertions(+), 31 deletions(-) diff --git a/members/nullnet-server/src/http_server/mod.rs b/members/nullnet-server/src/http_server/mod.rs index c6160d8..4198979 100644 --- a/members/nullnet-server/src/http_server/mod.rs +++ b/members/nullnet-server/src/http_server/mod.rs @@ -35,12 +35,12 @@ pub async fn serve(state: AppState) { .route("/api/health", get(health::health)) .route("/api/stacks", get(stacks::stacks_handler)) .route("/api/services/{stack}", get(services::services_handler)) - .route("/api/nodes", get(nodes::nodes_handler)) + .route("/api/nodes/{stack}", get(nodes::nodes_handler)) .route("/api/pool", get(pool::pool_handler)) .route("/api/config/{stack}", get(config::config_handler)) .route("/api/graph/{stack}", get(graph::graph_handler)) - .route("/api/sessions", get(sessions::list_handler)) - .route("/api/sessions/{id}", delete(sessions::teardown_handler)) + .route("/api/sessions/{stack}", get(sessions::list_handler)) + .route("/api/sessions/{stack}/{id}", delete(sessions::teardown_handler)) .route("/api/chains/{stack}", get(chains::chains_handler)) .route("/api/certificates", get(certificates::list_handler)) .route( diff --git a/members/nullnet-server/src/http_server/nodes.rs b/members/nullnet-server/src/http_server/nodes.rs index df71b54..53600ff 100644 --- a/members/nullnet-server/src/http_server/nodes.rs +++ b/members/nullnet-server/src/http_server/nodes.rs @@ -1,6 +1,6 @@ use super::AppState; use crate::services::service_info::ServiceInfo; -use axum::extract::State; +use axum::extract::{Path, State}; use axum::response::IntoResponse; use serde::Serialize; use std::collections::HashMap; @@ -18,14 +18,17 @@ struct NodeJson { hosted_services: Vec, } -pub(super) async fn nodes_handler(State(state): State) -> impl IntoResponse { +pub(super) async fn nodes_handler( + Path(stack): Path, + State(state): State, +) -> impl IntoResponse { let connected_ips = state.orchestrator.connected_node_ips().await; let services = state.services.read().await; let mut ip_services: HashMap> = connected_ips.iter().map(|ip| (*ip, vec![])).collect(); - for (stack, stack_map) in services.iter() { + if let Some(stack_map) = services.get(&stack) { for (name, info) in stack_map { if let ServiceInfo::Registered(reg) = info { for replica in reg.replicas() { diff --git a/members/nullnet-server/src/http_server/sessions.rs b/members/nullnet-server/src/http_server/sessions.rs index 77a95a7..ec4c8c0 100644 --- a/members/nullnet-server/src/http_server/sessions.rs +++ b/members/nullnet-server/src/http_server/sessions.rs @@ -24,11 +24,16 @@ struct ErrorJson { error: &'static str, } -pub(super) async fn list_handler(State(state): State) -> impl IntoResponse { +pub(super) async fn list_handler( + Path(stack): Path, + State(state): State, +) -> impl IntoResponse { let services = state.services.read().await; - let mut sessions: Vec = services - .values() - .flat_map(|stack_map| stack_map.iter()) + let Some(stack_map) = services.get(&stack) else { + return axum::Json(Vec::::new()).into_response(); + }; + let mut sessions: Vec = stack_map + .iter() .flat_map(|(name, info)| { if let ServiceInfo::Registered(reg) = info { reg.all_clients_owned() @@ -57,31 +62,38 @@ pub(super) async fn list_handler(State(state): State) -> impl IntoResp }) .collect(); sessions.sort_by_key(|s| s.id); - axum::Json(sessions) + axum::Json(sessions).into_response() } pub(super) async fn teardown_handler( State(state): State, - Path(id): Path, + Path((stack, id)): Path<(String, u32)>, ) -> impl IntoResponse { let mut services = state.services.write().await; - // Sessions span every stack; locate the (stack, service, client) triple - // that owns this NET id. - let found = services.iter().find_map(|(stack, stack_map)| { + let found = { + let Some(stack_map) = services.get(&stack) else { + return ( + StatusCode::NOT_FOUND, + axum::Json(ErrorJson { + error: "session not found", + }), + ) + .into_response(); + }; stack_map.iter().find_map(|(name, info)| { if let ServiceInfo::Registered(reg) = info { reg.all_clients_owned() .into_iter() .find(|(c, ci, _, _)| c.is_proxy().is_some() && ci.net_id() == id) - .map(|(client, _, _, _)| (stack.clone(), name.clone(), client)) + .map(|(client, _, _, _)| (name.clone(), client)) } else { None } }) - }); + }; - let Some((stack, name, client)) = found else { + let Some((name, client)) = found else { return ( StatusCode::NOT_FOUND, axum::Json(ErrorJson { diff --git a/members/nullnet-server/ui/src/components/Layout.tsx b/members/nullnet-server/ui/src/components/Layout.tsx index 7d13d38..61f0818 100644 --- a/members/nullnet-server/ui/src/components/Layout.tsx +++ b/members/nullnet-server/ui/src/components/Layout.tsx @@ -41,7 +41,7 @@ const NAV = [ export default function Layout({ page, topbarRight, children }: Props) { const { stack, setStack, editing, setEditing } = useStack(); - const { data: sessions } = useApi('/api/sessions', 5000); + const { data: sessions } = useApi(`/api/sessions/${stack}`, 5000); const { data: availableStacks } = useApi('/api/stacks', 10000); const [dropdownOpen, setDropdownOpen] = useState(false); const dropdownRef = useRef(null); diff --git a/members/nullnet-server/ui/src/components/topology/TopologyContext.tsx b/members/nullnet-server/ui/src/components/topology/TopologyContext.tsx index f4ceb2a..7ec51c2 100644 --- a/members/nullnet-server/ui/src/components/topology/TopologyContext.tsx +++ b/members/nullnet-server/ui/src/components/topology/TopologyContext.tsx @@ -111,7 +111,7 @@ export function TopologyProvider({ }) { const { data: graph, refetch } = useApi(`/api/graph/${stack}`); const { data: services } = useApi(`/api/services/${stack}`, 5000); - const { data: sessions } = useApi('/api/sessions', 5000); + const { data: sessions } = useApi(`/api/sessions/${stack}`, 5000); const { data: chains, refetch: refetchChains } = useApi(`/api/chains/${stack}`); const [uiState, dispatch] = useReducer(uiReducer, initialUIState); diff --git a/members/nullnet-server/ui/src/pages/Dashboard.tsx b/members/nullnet-server/ui/src/pages/Dashboard.tsx index 6875eaa..7ce38b7 100644 --- a/members/nullnet-server/ui/src/pages/Dashboard.tsx +++ b/members/nullnet-server/ui/src/pages/Dashboard.tsx @@ -7,9 +7,9 @@ import ZoomFrame from '../components/topology/ZoomFrame'; export default function Dashboard() { const { stack } = useStack(); - const { data: sessions } = useApi('/api/sessions', 5000); + const { data: sessions } = useApi(`/api/sessions/${stack}`, 5000); const { data: services } = useApi(`/api/services/${stack}`, 5000); - const { data: nodes } = useApi('/api/nodes', 5000); + const { data: nodes } = useApi(`/api/nodes/${stack}`, 5000); const { data: pool } = useApi('/api/pool', 5000); const { data: graph } = useApi(`/api/graph/${stack}`, 5000); diff --git a/members/nullnet-server/ui/src/pages/Nodes.tsx b/members/nullnet-server/ui/src/pages/Nodes.tsx index 402c7b7..5a2ec72 100644 --- a/members/nullnet-server/ui/src/pages/Nodes.tsx +++ b/members/nullnet-server/ui/src/pages/Nodes.tsx @@ -1,10 +1,12 @@ import { useState } from 'react'; import Layout from '../components/Layout'; import { useApi } from '../hooks/useApi'; +import { useStack } from '../StackContext'; import type { NodeJson } from '../types'; export default function Nodes() { - const { data: nodes, loading } = useApi('/api/nodes', 5000); + const { stack } = useStack(); + const { data: nodes, loading } = useApi(`/api/nodes/${stack}`, 5000); const [selected, setSelected] = useState(null); const selectedNode = nodes?.find(n => n.ip === selected) ?? null; @@ -56,12 +58,6 @@ export default function Nodes() {
Services
{node.hosted_services.length}
-
-
Stacks
-
- {new Set(node.hosted_services.map(s => s.stack)).size} -
-
{node.hosted_services.length > 0 && ( diff --git a/members/nullnet-server/ui/src/pages/Sessions.tsx b/members/nullnet-server/ui/src/pages/Sessions.tsx index d544895..5d6b279 100644 --- a/members/nullnet-server/ui/src/pages/Sessions.tsx +++ b/members/nullnet-server/ui/src/pages/Sessions.tsx @@ -1,17 +1,19 @@ import { useState } from 'react'; import Layout from '../components/Layout'; import { useApi } from '../hooks/useApi'; +import { useStack } from '../StackContext'; import type { SessionJson } from '../types'; export default function Sessions() { - const { data: sessions, loading, refetch } = useApi('/api/sessions', 5000); + const { stack } = useStack(); + const { data: sessions, loading, refetch } = useApi(`/api/sessions/${stack}`, 5000); const [tearing, setTearing] = useState>(new Set()); async function teardown(id: number) { if (!confirm(`Force teardown session ${id}?`)) return; setTearing(prev => new Set(prev).add(id)); try { - await fetch(`/api/sessions/${id}`, { method: 'DELETE' }); + await fetch(`/api/sessions/${stack}/${id}`, { method: 'DELETE' }); refetch(); } finally { setTearing(prev => { const next = new Set(prev); next.delete(id); return next; }); From 575002b896f981169285a107e1729d449ecaaf6c Mon Sep 17 00:00:00 2001 From: Anton Liashkevich Date: Tue, 23 Jun 2026 13:46:41 -0400 Subject: [PATCH 2/2] Fmt fixed --- members/nullnet-server/src/http_server/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/members/nullnet-server/src/http_server/mod.rs b/members/nullnet-server/src/http_server/mod.rs index 4198979..3d1ed61 100644 --- a/members/nullnet-server/src/http_server/mod.rs +++ b/members/nullnet-server/src/http_server/mod.rs @@ -40,7 +40,10 @@ pub async fn serve(state: AppState) { .route("/api/config/{stack}", get(config::config_handler)) .route("/api/graph/{stack}", get(graph::graph_handler)) .route("/api/sessions/{stack}", get(sessions::list_handler)) - .route("/api/sessions/{stack}/{id}", delete(sessions::teardown_handler)) + .route( + "/api/sessions/{stack}/{id}", + delete(sessions::teardown_handler), + ) .route("/api/chains/{stack}", get(chains::chains_handler)) .route("/api/certificates", get(certificates::list_handler)) .route(