diff --git a/package/secubox/secubox-p2p/Makefile b/package/secubox/secubox-p2p/Makefile index d3c478e3..7a82073d 100644 --- a/package/secubox/secubox-p2p/Makefile +++ b/package/secubox/secubox-p2p/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=secubox-p2p -PKG_VERSION:=0.4.0 +PKG_VERSION:=0.5.0 PKG_RELEASE:=1 PKG_MAINTAINER:=SecuBox Team @@ -21,8 +21,9 @@ define Package/secubox-p2p/description SecuBox P2P Hub backend providing peer discovery, mesh networking, DNS federation, and distributed service management. Includes mDNS service announcement, REST API on port 7331 for mesh visibility, - and SecuBox Factory unified dashboard with Ed25519 signed Merkle - snapshots for cryptographic configuration validation. + SecuBox Factory unified dashboard with Ed25519 signed Merkle + snapshots for cryptographic configuration validation, and distributed + mesh services panel for aggregated service discovery across all nodes. endef define Package/secubox-p2p/conffiles @@ -64,6 +65,7 @@ define Package/secubox-p2p/install $(INSTALL_BIN) ./root/www/api/factory/run $(1)/www/api/factory/ $(INSTALL_BIN) ./root/www/api/factory/snapshot $(1)/www/api/factory/ $(INSTALL_BIN) ./root/www/api/factory/pubkey $(1)/www/api/factory/ + $(INSTALL_BIN) ./root/www/api/factory/mesh-services $(1)/www/api/factory/ # Factory Web UI $(INSTALL_DIR) $(1)/www/factory diff --git a/package/secubox/secubox-p2p/root/www/api/factory/mesh-services b/package/secubox/secubox-p2p/root/www/api/factory/mesh-services new file mode 100644 index 00000000..f979f498 --- /dev/null +++ b/package/secubox/secubox-p2p/root/www/api/factory/mesh-services @@ -0,0 +1,134 @@ +#!/bin/sh +# Factory Mesh Services - Aggregated service discovery across mesh +# CGI endpoint for distributed service panel + +echo "Content-Type: application/json" +echo "Access-Control-Allow-Origin: *" +echo "Access-Control-Allow-Methods: GET, OPTIONS" +echo "" + +# Handle CORS preflight +if [ "$REQUEST_METHOD" = "OPTIONS" ]; then + exit 0 +fi + +# Get local node ID +P2P_STATE_DIR="/var/run/secubox-p2p" +LOCAL_NODE_ID=$(cat "$P2P_STATE_DIR/node.id" 2>/dev/null || hostname) +LOCAL_NODE_NAME=$(uci -q get system.@system[0].hostname || hostname) + +# Get local services +get_local_services() { + local services_json=$(/usr/sbin/secubox-p2p services 2>/dev/null) + if [ -z "$services_json" ]; then + echo '{"services":[]}' + return + fi + echo "$services_json" +} + +# Get services from a peer +get_peer_services() { + local peer_addr="$1" + local peer_name="$2" + + # Query peer's services endpoint with timeout + local peer_services=$(curl -s --connect-timeout 3 --max-time 5 "http://$peer_addr:7331/api/services" 2>/dev/null) + + if [ -n "$peer_services" ] && echo "$peer_services" | grep -q "services"; then + echo "$peer_services" + else + echo '{"services":[],"error":"unreachable"}' + fi +} + +# Build aggregated mesh services +build_mesh_services() { + local result='{"nodes":[' + local first_node=1 + + # Add local node + local local_services=$(get_local_services) + local local_svc_list=$(echo "$local_services" | jsonfilter -e '@.services[*]' 2>/dev/null) + local local_svc_count=$(echo "$local_services" | jsonfilter -e '@.services[*]' 2>/dev/null | wc -l) + + result="$result{\"node_id\":\"$LOCAL_NODE_ID\",\"node_name\":\"$LOCAL_NODE_NAME\",\"is_local\":true,\"status\":\"online\"," + result="$result\"services\":[" + + local i=0 + while [ $i -lt $local_svc_count ]; do + local svc_name=$(echo "$local_services" | jsonfilter -e "@.services[$i].name" 2>/dev/null) + local svc_status=$(echo "$local_services" | jsonfilter -e "@.services[$i].status" 2>/dev/null) + local svc_enabled=$(echo "$local_services" | jsonfilter -e "@.services[$i].enabled" 2>/dev/null) + local svc_port=$(echo "$local_services" | jsonfilter -e "@.services[$i].port" 2>/dev/null) + + [ $i -gt 0 ] && result="$result," + result="$result{\"name\":\"$svc_name\",\"status\":\"$svc_status\",\"enabled\":$svc_enabled,\"port\":\"$svc_port\"}" + i=$((i + 1)) + done + + result="$result]}" + first_node=0 + + # Get peer services + local peers_file="/tmp/secubox-p2p-peers.json" + if [ -f "$peers_file" ]; then + local peer_count=$(jsonfilter -i "$peers_file" -e '@.peers[*]' 2>/dev/null | wc -l) + local p=0 + + while [ $p -lt $peer_count ]; do + local peer_addr=$(jsonfilter -i "$peers_file" -e "@.peers[$p].address" 2>/dev/null) + local peer_name=$(jsonfilter -i "$peers_file" -e "@.peers[$p].name" 2>/dev/null) + local peer_id=$(jsonfilter -i "$peers_file" -e "@.peers[$p].id" 2>/dev/null) + local is_local=$(jsonfilter -i "$peers_file" -e "@.peers[$p].is_local" 2>/dev/null) + + # Skip local node in peers list + if [ "$is_local" != "true" ] && [ -n "$peer_addr" ]; then + local peer_services=$(get_peer_services "$peer_addr" "$peer_name") + local peer_status="offline" + local peer_error="" + + if echo "$peer_services" | grep -q '"error"'; then + peer_error=$(echo "$peer_services" | jsonfilter -e '@.error' 2>/dev/null) + else + peer_status="online" + fi + + result="$result,{\"node_id\":\"${peer_id:-$peer_addr}\",\"node_name\":\"${peer_name:-$peer_addr}\",\"address\":\"$peer_addr\",\"is_local\":false,\"status\":\"$peer_status\"," + + if [ "$peer_status" = "online" ]; then + # Parse peer services + local peer_svc_count=$(echo "$peer_services" | jsonfilter -e '@.services[*]' 2>/dev/null | wc -l) + result="$result\"services\":[" + + local j=0 + while [ $j -lt $peer_svc_count ]; do + local svc_name=$(echo "$peer_services" | jsonfilter -e "@.services[$j].name" 2>/dev/null) + local svc_status=$(echo "$peer_services" | jsonfilter -e "@.services[$j].status" 2>/dev/null) + local svc_enabled=$(echo "$peer_services" | jsonfilter -e "@.services[$j].enabled" 2>/dev/null) + local svc_port=$(echo "$peer_services" | jsonfilter -e "@.services[$j].port" 2>/dev/null) + + [ $j -gt 0 ] && result="$result," + result="$result{\"name\":\"$svc_name\",\"status\":\"$svc_status\",\"enabled\":${svc_enabled:-0},\"port\":\"$svc_port\"}" + j=$((j + 1)) + done + + result="$result]}" + else + result="$result\"services\":[],\"error\":\"$peer_error\"}" + fi + fi + p=$((p + 1)) + done + fi + + result="$result]," + + # Add summary stats + result="$result\"summary\":{\"total_nodes\":0,\"online_nodes\":0,\"total_services\":0,\"running_services\":0}}" + + echo "$result" +} + +# Output aggregated mesh services +build_mesh_services diff --git a/package/secubox/secubox-p2p/root/www/factory/index.html b/package/secubox/secubox-p2p/root/www/factory/index.html index 5b7306ee..37bfd0ae 100644 --- a/package/secubox/secubox-p2p/root/www/factory/index.html +++ b/package/secubox/secubox-p2p/root/www/factory/index.html @@ -97,6 +97,46 @@ /* Fingerprint display */ .fingerprint { font-family: ui-monospace, monospace; font-size: 0.8rem; background: var(--bg); padding: 0.35rem 0.5rem; border-radius: 0.25rem; letter-spacing: 0.05em; } + + /* Tab navigation */ + .tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; } + .tab { padding: 0.5rem 1rem; background: transparent; border: none; color: var(--muted); cursor: pointer; font-weight: 500; position: relative; } + .tab.active { color: var(--accent); } + .tab.active::after { content: ''; position: absolute; bottom: -0.5rem; left: 0; right: 0; height: 2px; background: var(--accent); } + .tab:hover { color: var(--text); } + .tab-content { display: none; } + .tab-content.active { display: block; } + + /* Services mesh panel */ + .services-grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } + .node-services { background: var(--card); border-radius: 0.5rem; overflow: hidden; } + .node-services-header { padding: 0.75rem 1rem; background: rgba(0,0,0,0.2); display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); } + .node-services-header h4 { font-size: 0.9rem; font-weight: 600; } + .services-list { padding: 0.5rem; max-height: 300px; overflow-y: auto; } + .service-item { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; border-radius: 0.25rem; margin-bottom: 0.25rem; background: rgba(0,0,0,0.15); } + .service-item:hover { background: rgba(0,0,0,0.3); } + .service-info { flex: 1; min-width: 0; } + .service-name { font-size: 0.85rem; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .service-port { font-size: 0.7rem; color: var(--muted); font-family: ui-monospace, monospace; } + .service-status { display: flex; align-items: center; gap: 0.5rem; } + .status-dot { width: 8px; height: 8px; border-radius: 50%; } + .status-dot.running { background: var(--success); } + .status-dot.stopped { background: var(--danger); } + .status-dot.disabled { background: var(--muted); } + .service-link { font-size: 0.7rem; padding: 0.2rem 0.4rem; background: var(--accent); border-radius: 0.15rem; text-decoration: none; color: var(--text); white-space: nowrap; } + .service-link:hover { background: #818cf8; } + + /* Services summary bar */ + .services-summary { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; padding: 1rem; background: var(--card); border-radius: 0.5rem; } + .summary-item { display: flex; align-items: center; gap: 0.5rem; } + .summary-count { font-size: 1.25rem; font-weight: 700; color: var(--accent); } + .summary-label { font-size: 0.75rem; color: var(--muted); } + + /* Search/filter */ + .filter-bar { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; } + .filter-input { flex: 1; min-width: 200px; padding: 0.5rem 0.75rem; background: var(--card); border: 1px solid var(--border); border-radius: 0.25rem; color: var(--text); font-size: 0.85rem; } + .filter-input:focus { outline: none; border-color: var(--accent); } + .filter-select { padding: 0.5rem 0.75rem; background: var(--card); border: 1px solid var(--border); border-radius: 0.25rem; color: var(--text); font-size: 0.85rem; }
@@ -113,9 +153,22 @@ -