feat(p2p): Add MirrorBox NetMesh Catalog with DNS federation
Implement distributed service catalog that discovers HAProxy vhosts
and provides multi-endpoint access URLs (haproxy/mesh/local). Add
dynamic DNS federation that auto-populates dnsmasq with mesh peer
hostnames (hostname.mesh.local).
New features:
- /factory/catalog API endpoint with service registry
- Catalog tab (📚) in Factory UI with endpoint filtering
- QR codes with URL type switching (haproxy/mesh/local)
- Linked mesh peers navigation panel
- DNS federation via /tmp/hosts/secubox-mesh
- CLI commands: dns-enable/disable/update, catalog sync/list/generate
Bumps secubox-p2p to v0.6.0.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cf115b346a
commit
eec83efa13
@ -20,7 +20,7 @@ Architecture: all
|
||||
Installed-Size: 378880
|
||||
Description: Advanced bandwidth management with QoS rules, client quotas, and SQM integration
|
||||
Filename: luci-app-bandwidth-manager_0.5.0-r2_all.ipk
|
||||
Size: 66966
|
||||
Size: 66972
|
||||
|
||||
Package: luci-app-cdn-cache
|
||||
Version: 0.5.0-r3
|
||||
@ -32,7 +32,7 @@ Architecture: all
|
||||
Installed-Size: 122880
|
||||
Description: Dashboard for managing local CDN caching proxy on OpenWrt
|
||||
Filename: luci-app-cdn-cache_0.5.0-r3_all.ipk
|
||||
Size: 23189
|
||||
Size: 23190
|
||||
|
||||
Package: luci-app-client-guardian
|
||||
Version: 0.4.0-r7
|
||||
@ -44,7 +44,7 @@ Architecture: all
|
||||
Installed-Size: 307200
|
||||
Description: Network Access Control with client monitoring, zone management, captive portal, parental controls, and SMS/email alerts
|
||||
Filename: luci-app-client-guardian_0.4.0-r7_all.ipk
|
||||
Size: 57042
|
||||
Size: 57045
|
||||
|
||||
Package: luci-app-crowdsec-dashboard
|
||||
Version: 0.7.0-r29
|
||||
@ -56,7 +56,7 @@ Architecture: all
|
||||
Installed-Size: 296960
|
||||
Description: Real-time security monitoring dashboard for CrowdSec on OpenWrt
|
||||
Filename: luci-app-crowdsec-dashboard_0.7.0-r29_all.ipk
|
||||
Size: 55585
|
||||
Size: 55583
|
||||
|
||||
Package: luci-app-cyberfeed
|
||||
Version: 0.1.1-r1
|
||||
@ -68,7 +68,7 @@ Architecture: all
|
||||
Installed-Size: 71680
|
||||
Description: Cyberpunk-themed RSS feed aggregator dashboard with social media support
|
||||
Filename: luci-app-cyberfeed_0.1.1-r1_all.ipk
|
||||
Size: 12838
|
||||
Size: 12841
|
||||
|
||||
Package: luci-app-exposure
|
||||
Version: 1.0.0-r3
|
||||
@ -92,7 +92,7 @@ Architecture: all
|
||||
Installed-Size: 92160
|
||||
Description: Modern dashboard for Gitea Platform management on OpenWrt
|
||||
Filename: luci-app-gitea_1.0.0-r2_all.ipk
|
||||
Size: 15585
|
||||
Size: 15589
|
||||
|
||||
Package: luci-app-glances
|
||||
Version: 1.0.0-r2
|
||||
@ -104,7 +104,7 @@ Architecture: all
|
||||
Installed-Size: 40960
|
||||
Description: Modern dashboard for Glances system monitoring with SecuBox theme
|
||||
Filename: luci-app-glances_1.0.0-r2_all.ipk
|
||||
Size: 6963
|
||||
Size: 6968
|
||||
|
||||
Package: luci-app-haproxy
|
||||
Version: 1.0.0-r8
|
||||
@ -116,7 +116,7 @@ Architecture: all
|
||||
Installed-Size: 204800
|
||||
Description: Web interface for managing HAProxy load balancer with vhosts, SSL certificates, and backend routing
|
||||
Filename: luci-app-haproxy_1.0.0-r8_all.ipk
|
||||
Size: 34168
|
||||
Size: 34166
|
||||
|
||||
Package: luci-app-hexojs
|
||||
Version: 1.0.0-r3
|
||||
@ -128,7 +128,7 @@ Architecture: all
|
||||
Installed-Size: 215040
|
||||
Description: Modern dashboard for Hexo static site generator on OpenWrt
|
||||
Filename: luci-app-hexojs_1.0.0-r3_all.ipk
|
||||
Size: 32974
|
||||
Size: 32975
|
||||
|
||||
Package: luci-app-ksm-manager
|
||||
Version: 0.4.0-r2
|
||||
@ -140,7 +140,7 @@ Architecture: all
|
||||
Installed-Size: 112640
|
||||
Description: Centralized cryptographic key management with hardware security module (HSM) support for Nitrokey and YubiKey devices. Provides secure key storage, certificate management, SSH key handling, and secret storage with audit logging.
|
||||
Filename: luci-app-ksm-manager_0.4.0-r2_all.ipk
|
||||
Size: 18720
|
||||
Size: 18721
|
||||
|
||||
Package: luci-app-localai
|
||||
Version: 0.1.0-r15
|
||||
@ -164,7 +164,7 @@ Architecture: all
|
||||
Installed-Size: 40960
|
||||
Description: LuCI support for Lyrion Music Server
|
||||
Filename: luci-app-lyrion_1.0.0-r1_all.ipk
|
||||
Size: 6725
|
||||
Size: 6730
|
||||
|
||||
Package: luci-app-magicmirror2
|
||||
Version: 0.4.0-r6
|
||||
@ -176,7 +176,7 @@ Architecture: all
|
||||
Installed-Size: 71680
|
||||
Description: Modern dashboard for MagicMirror2 smart display platform with module manager and SecuBox theme
|
||||
Filename: luci-app-magicmirror2_0.4.0-r6_all.ipk
|
||||
Size: 12274
|
||||
Size: 12277
|
||||
|
||||
Package: luci-app-mailinabox
|
||||
Version: 1.0.0-r1
|
||||
@ -188,7 +188,7 @@ Architecture: all
|
||||
Installed-Size: 30720
|
||||
Description: LuCI support for Mail-in-a-Box
|
||||
Filename: luci-app-mailinabox_1.0.0-r1_all.ipk
|
||||
Size: 5481
|
||||
Size: 5482
|
||||
|
||||
Package: luci-app-media-flow
|
||||
Version: 0.6.4-r1
|
||||
@ -200,7 +200,7 @@ Architecture: all
|
||||
Installed-Size: 102400
|
||||
Description: Real-time detection and monitoring of streaming services (Netflix, YouTube, Spotify, etc.) with quality estimation, history tracking, and alerts. Supports nDPId local DPI and netifyd.
|
||||
Filename: luci-app-media-flow_0.6.4-r1_all.ipk
|
||||
Size: 19120
|
||||
Size: 19117
|
||||
|
||||
Package: luci-app-metablogizer
|
||||
Version: 1.0.0-r3
|
||||
@ -212,7 +212,7 @@ Architecture: all
|
||||
Installed-Size: 112640
|
||||
Description: LuCI support for MetaBlogizer Static Site Publisher
|
||||
Filename: luci-app-metablogizer_1.0.0-r3_all.ipk
|
||||
Size: 23503
|
||||
Size: 23501
|
||||
|
||||
Package: luci-app-metabolizer
|
||||
Version: 1.0.0-r2
|
||||
@ -236,7 +236,7 @@ Architecture: all
|
||||
Installed-Size: 102400
|
||||
Description: Modern dashboard for mitmproxy HTTPS traffic inspection with SecuBox theme
|
||||
Filename: luci-app-mitmproxy_0.4.0-r6_all.ipk
|
||||
Size: 18933
|
||||
Size: 18932
|
||||
|
||||
Package: luci-app-mmpm
|
||||
Version: 0.2.0-r3
|
||||
@ -248,7 +248,7 @@ Architecture: all
|
||||
Installed-Size: 51200
|
||||
Description: Web interface for MMPM - MagicMirror Package Manager
|
||||
Filename: luci-app-mmpm_0.2.0-r3_all.ipk
|
||||
Size: 7899
|
||||
Size: 7901
|
||||
|
||||
Package: luci-app-mqtt-bridge
|
||||
Version: 0.4.0-r4
|
||||
@ -260,7 +260,7 @@ Architecture: all
|
||||
Installed-Size: 122880
|
||||
Description: USB-to-MQTT IoT hub with SecuBox theme
|
||||
Filename: luci-app-mqtt-bridge_0.4.0-r4_all.ipk
|
||||
Size: 22777
|
||||
Size: 22779
|
||||
|
||||
Package: luci-app-ndpid
|
||||
Version: 1.1.2-r2
|
||||
@ -272,7 +272,7 @@ Architecture: all
|
||||
Installed-Size: 122880
|
||||
Description: Modern dashboard for nDPId deep packet inspection on OpenWrt
|
||||
Filename: luci-app-ndpid_1.1.2-r2_all.ipk
|
||||
Size: 22455
|
||||
Size: 22457
|
||||
|
||||
Package: luci-app-netdata-dashboard
|
||||
Version: 0.5.0-r2
|
||||
@ -284,7 +284,7 @@ Architecture: all
|
||||
Installed-Size: 133120
|
||||
Description: Real-time system monitoring dashboard with Netdata integration for OpenWrt
|
||||
Filename: luci-app-netdata-dashboard_0.5.0-r2_all.ipk
|
||||
Size: 22396
|
||||
Size: 22400
|
||||
|
||||
Package: luci-app-network-modes
|
||||
Version: 0.5.0-r3
|
||||
@ -296,7 +296,7 @@ Architecture: all
|
||||
Installed-Size: 307200
|
||||
Description: Configure OpenWrt for different network modes: Sniffer, Access Point, Relay, Router
|
||||
Filename: luci-app-network-modes_0.5.0-r3_all.ipk
|
||||
Size: 55608
|
||||
Size: 55613
|
||||
|
||||
Package: luci-app-network-tweaks
|
||||
Version: 1.0.0-r7
|
||||
@ -308,7 +308,7 @@ Architecture: all
|
||||
Installed-Size: 81920
|
||||
Description: Unified network services dashboard with DNS/hosts sync, CDN cache control, and WPAD auto-proxy configuration
|
||||
Filename: luci-app-network-tweaks_1.0.0-r7_all.ipk
|
||||
Size: 15455
|
||||
Size: 15466
|
||||
|
||||
Package: luci-app-nextcloud
|
||||
Version: 1.0.0-r1
|
||||
@ -320,7 +320,7 @@ Architecture: all
|
||||
Installed-Size: 30720
|
||||
Description: LuCI support for Nextcloud
|
||||
Filename: luci-app-nextcloud_1.0.0-r1_all.ipk
|
||||
Size: 6485
|
||||
Size: 6487
|
||||
|
||||
Package: luci-app-ollama
|
||||
Version: 0.1.0-r1
|
||||
@ -332,7 +332,7 @@ Architecture: all
|
||||
Installed-Size: 71680
|
||||
Description: Modern dashboard for Ollama LLM management on OpenWrt
|
||||
Filename: luci-app-ollama_0.1.0-r1_all.ipk
|
||||
Size: 11991
|
||||
Size: 11998
|
||||
|
||||
Package: luci-app-picobrew
|
||||
Version: 1.0.0-r1
|
||||
@ -344,7 +344,7 @@ Architecture: all
|
||||
Installed-Size: 51200
|
||||
Description: Modern dashboard for PicoBrew Server management on OpenWrt
|
||||
Filename: luci-app-picobrew_1.0.0-r1_all.ipk
|
||||
Size: 9972
|
||||
Size: 9979
|
||||
|
||||
Package: luci-app-secubox
|
||||
Version: 0.7.1-r4
|
||||
@ -356,7 +356,7 @@ Architecture: all
|
||||
Installed-Size: 266240
|
||||
Description: Central control hub for all SecuBox modules. Provides unified dashboard, module status, system health monitoring, and quick actions.
|
||||
Filename: luci-app-secubox_0.7.1-r4_all.ipk
|
||||
Size: 49900
|
||||
Size: 49901
|
||||
|
||||
Package: luci-app-secubox-admin
|
||||
Version: 1.0.0-r19
|
||||
@ -367,7 +367,7 @@ Architecture: all
|
||||
Installed-Size: 337920
|
||||
Description: Unified admin control center for SecuBox appstore plugins with system monitoring
|
||||
Filename: luci-app-secubox-admin_1.0.0-r19_all.ipk
|
||||
Size: 57094
|
||||
Size: 57098
|
||||
|
||||
Package: luci-app-secubox-crowdsec
|
||||
Version: 1.0.0-r3
|
||||
@ -379,7 +379,7 @@ Architecture: all
|
||||
Installed-Size: 81920
|
||||
Description: LuCI SecuBox CrowdSec Dashboard
|
||||
Filename: luci-app-secubox-crowdsec_1.0.0-r3_all.ipk
|
||||
Size: 13914
|
||||
Size: 13925
|
||||
|
||||
Package: luci-app-secubox-netdiag
|
||||
Version: 1.0.0-r1
|
||||
@ -391,7 +391,7 @@ Architecture: all
|
||||
Installed-Size: 61440
|
||||
Description: Real-time DSA switch port statistics, error monitoring, and network health diagnostics
|
||||
Filename: luci-app-secubox-netdiag_1.0.0-r1_all.ipk
|
||||
Size: 12000
|
||||
Size: 12002
|
||||
|
||||
Package: luci-app-secubox-netifyd
|
||||
Version: 1.2.1-r1
|
||||
@ -415,7 +415,7 @@ Architecture: all
|
||||
Installed-Size: 215040
|
||||
Description: LuCI SecuBox P2P Hub
|
||||
Filename: luci-app-secubox-p2p_0.1.0-r1_all.ipk
|
||||
Size: 39254
|
||||
Size: 39257
|
||||
|
||||
Package: luci-app-secubox-portal
|
||||
Version: 0.7.0-r2
|
||||
@ -427,7 +427,7 @@ Architecture: all
|
||||
Installed-Size: 122880
|
||||
Description: Unified entry point for all SecuBox applications with tabbed navigation
|
||||
Filename: luci-app-secubox-portal_0.7.0-r2_all.ipk
|
||||
Size: 24553
|
||||
Size: 24557
|
||||
|
||||
Package: luci-app-secubox-security-threats
|
||||
Version: 1.0.0-r4
|
||||
@ -439,7 +439,7 @@ Architecture: all
|
||||
Installed-Size: 71680
|
||||
Description: Unified dashboard integrating netifyd DPI threats with CrowdSec intelligence for real-time threat monitoring and automated blocking
|
||||
Filename: luci-app-secubox-security-threats_1.0.0-r4_all.ipk
|
||||
Size: 13899
|
||||
Size: 13906
|
||||
|
||||
Package: luci-app-service-registry
|
||||
Version: 1.0.0-r1
|
||||
@ -451,7 +451,7 @@ Architecture: all
|
||||
Installed-Size: 194560
|
||||
Description: Unified service aggregation with HAProxy vhosts, Tor hidden services, and QR-coded landing page
|
||||
Filename: luci-app-service-registry_1.0.0-r1_all.ipk
|
||||
Size: 39826
|
||||
Size: 39827
|
||||
|
||||
Package: luci-app-streamlit
|
||||
Version: 1.0.0-r9
|
||||
@ -463,7 +463,7 @@ Architecture: all
|
||||
Installed-Size: 122880
|
||||
Description: Modern dashboard for Streamlit Platform management on OpenWrt
|
||||
Filename: luci-app-streamlit_1.0.0-r9_all.ipk
|
||||
Size: 20470
|
||||
Size: 20471
|
||||
|
||||
Package: luci-app-system-hub
|
||||
Version: 0.5.1-r4
|
||||
@ -475,7 +475,7 @@ Architecture: all
|
||||
Installed-Size: 358400
|
||||
Description: Central system control with monitoring, services, logs, and backup
|
||||
Filename: luci-app-system-hub_0.5.1-r4_all.ipk
|
||||
Size: 66345
|
||||
Size: 66351
|
||||
|
||||
Package: luci-app-tor-shield
|
||||
Version: 1.0.0-r10
|
||||
@ -487,7 +487,7 @@ Architecture: all
|
||||
Installed-Size: 133120
|
||||
Description: Modern dashboard for Tor anonymization on OpenWrt
|
||||
Filename: luci-app-tor-shield_1.0.0-r10_all.ipk
|
||||
Size: 24532
|
||||
Size: 24537
|
||||
|
||||
Package: luci-app-traffic-shaper
|
||||
Version: 0.4.0-r2
|
||||
@ -499,7 +499,7 @@ Architecture: all
|
||||
Installed-Size: 92160
|
||||
Description: Advanced traffic shaping with TC/CAKE for precise bandwidth control
|
||||
Filename: luci-app-traffic-shaper_0.4.0-r2_all.ipk
|
||||
Size: 15635
|
||||
Size: 15637
|
||||
|
||||
Package: luci-app-vhost-manager
|
||||
Version: 0.5.0-r5
|
||||
@ -511,7 +511,7 @@ Architecture: all
|
||||
Installed-Size: 153600
|
||||
Description: Nginx reverse proxy manager with Let's Encrypt SSL certificates, authentication, and WebSocket support
|
||||
Filename: luci-app-vhost-manager_0.5.0-r5_all.ipk
|
||||
Size: 26199
|
||||
Size: 26204
|
||||
|
||||
Package: luci-app-wireguard-dashboard
|
||||
Version: 0.7.0-r5
|
||||
@ -523,7 +523,7 @@ Architecture: all
|
||||
Installed-Size: 235520
|
||||
Description: Modern dashboard for WireGuard VPN monitoring on OpenWrt
|
||||
Filename: luci-app-wireguard-dashboard_0.7.0-r5_all.ipk
|
||||
Size: 45368
|
||||
Size: 45372
|
||||
|
||||
Package: luci-app-zigbee2mqtt
|
||||
Version: 1.0.0-r2
|
||||
@ -535,7 +535,7 @@ Architecture: all
|
||||
Installed-Size: 40960
|
||||
Description: Graphical interface for managing the Zigbee2MQTT docker application.
|
||||
Filename: luci-app-zigbee2mqtt_1.0.0-r2_all.ipk
|
||||
Size: 7085
|
||||
Size: 7095
|
||||
|
||||
Package: luci-theme-secubox
|
||||
Version: 0.4.7-r1
|
||||
@ -547,7 +547,7 @@ Architecture: all
|
||||
Installed-Size: 460800
|
||||
Description: Global CyberMood design system (CSS/JS/i18n) shared by all SecuBox dashboards.
|
||||
Filename: luci-theme-secubox_0.4.7-r1_all.ipk
|
||||
Size: 111793
|
||||
Size: 111798
|
||||
|
||||
Package: secubox-app
|
||||
Version: 1.0.0-r2
|
||||
@ -558,7 +558,7 @@ Installed-Size: 92160
|
||||
Description: Command line helper for SecuBox App Store manifests. Installs /usr/sbin/secubox-app
|
||||
and ships the default manifests under /usr/share/secubox/plugins/.
|
||||
Filename: secubox-app_1.0.0-r2_all.ipk
|
||||
Size: 11185
|
||||
Size: 11181
|
||||
|
||||
Package: secubox-app-adguardhome
|
||||
Version: 1.0.0-r2
|
||||
@ -572,7 +572,7 @@ Description: Installer, configuration, and service manager for running AdGuard
|
||||
inside Docker on SecuBox-powered OpenWrt systems. Network-wide ad blocker
|
||||
with DNS-over-HTTPS/TLS support and detailed analytics.
|
||||
Filename: secubox-app-adguardhome_1.0.0-r2_all.ipk
|
||||
Size: 2876
|
||||
Size: 2883
|
||||
|
||||
Package: secubox-app-auth-logger
|
||||
Version: 1.2.2-r1
|
||||
@ -590,7 +590,7 @@ Description: Logs authentication failures from LuCI/rpcd and Dropbear SSH
|
||||
- JavaScript hook to intercept login failures
|
||||
- CrowdSec parser and bruteforce scenario
|
||||
Filename: secubox-app-auth-logger_1.2.2-r1_all.ipk
|
||||
Size: 9378
|
||||
Size: 9379
|
||||
|
||||
Package: secubox-app-crowdsec-custom
|
||||
Version: 1.1.0-r1
|
||||
@ -613,7 +613,7 @@ Description: Custom CrowdSec configurations for SecuBox web interface protectio
|
||||
- Webapp generic auth bruteforce protection
|
||||
- Whitelist for trusted networks
|
||||
Filename: secubox-app-crowdsec-custom_1.1.0-r1_all.ipk
|
||||
Size: 5759
|
||||
Size: 5767
|
||||
|
||||
Package: secubox-app-cs-firewall-bouncer
|
||||
Version: 0.0.31-r4
|
||||
@ -640,7 +640,7 @@ Description: SecuBox CrowdSec Firewall Bouncer for OpenWrt.
|
||||
- Automatic restart on firewall reload
|
||||
- procd service management
|
||||
Filename: secubox-app-cs-firewall-bouncer_0.0.31-r4_aarch64_cortex-a72.ipk
|
||||
Size: 5049321
|
||||
Size: 5049326
|
||||
|
||||
Package: secubox-app-cyberfeed
|
||||
Version: 0.2.1-r1
|
||||
@ -654,7 +654,7 @@ Description: Cyberpunk-themed RSS feed aggregator for OpenWrt/SecuBox.
|
||||
Features emoji injection, neon styling, and RSS-Bridge support
|
||||
for social media feeds (Facebook, Twitter, Mastodon).
|
||||
Filename: secubox-app-cyberfeed_0.2.1-r1_all.ipk
|
||||
Size: 12451
|
||||
Size: 12453
|
||||
|
||||
Package: secubox-app-domoticz
|
||||
Version: 1.0.0-r2
|
||||
@ -667,7 +667,7 @@ Installed-Size: 10240
|
||||
Description: Installer, configuration, and service manager for running Domoticz
|
||||
inside Docker on SecuBox-powered OpenWrt systems.
|
||||
Filename: secubox-app-domoticz_1.0.0-r2_all.ipk
|
||||
Size: 2544
|
||||
Size: 2553
|
||||
|
||||
Package: secubox-app-exposure
|
||||
Version: 1.0.0-r1
|
||||
@ -682,7 +682,7 @@ Description: Unified service exposure manager for SecuBox.
|
||||
- Dynamic Tor hidden service management
|
||||
- HAProxy SSL reverse proxy configuration
|
||||
Filename: secubox-app-exposure_1.0.0-r1_all.ipk
|
||||
Size: 6825
|
||||
Size: 6838
|
||||
|
||||
Package: secubox-app-gitea
|
||||
Version: 1.0.0-r5
|
||||
@ -772,7 +772,7 @@ Description: Hexo CMS - Self-hosted static blog generator for OpenWrt
|
||||
Runs in LXC container with Alpine Linux.
|
||||
Configure in /etc/config/hexojs.
|
||||
Filename: secubox-app-hexojs_1.0.0-r8_all.ipk
|
||||
Size: 94934
|
||||
Size: 94935
|
||||
|
||||
Package: secubox-app-localai
|
||||
Version: 2.25.0-r1
|
||||
@ -794,7 +794,7 @@ Description: LocalAI native binary package for OpenWrt.
|
||||
|
||||
API: http://<router-ip>:8081/v1
|
||||
Filename: secubox-app-localai_2.25.0-r1_all.ipk
|
||||
Size: 5712
|
||||
Size: 5721
|
||||
|
||||
Package: secubox-app-localai-wb
|
||||
Version: 2.25.0-r1
|
||||
@ -818,7 +818,7 @@ Description: LocalAI native binary package for OpenWrt.
|
||||
|
||||
API: http://<router-ip>:8080/v1
|
||||
Filename: secubox-app-localai-wb_2.25.0-r1_all.ipk
|
||||
Size: 7950
|
||||
Size: 7956
|
||||
|
||||
Package: secubox-app-lyrion
|
||||
Version: 2.0.2-r1
|
||||
@ -838,7 +838,7 @@ Description: Lyrion Media Server (formerly Logitech Media Server / Squeezebox S
|
||||
Auto-detects available runtime, preferring LXC for lower resource usage.
|
||||
Configure runtime in /etc/config/lyrion.
|
||||
Filename: secubox-app-lyrion_2.0.2-r1_all.ipk
|
||||
Size: 7285
|
||||
Size: 7289
|
||||
|
||||
Package: secubox-app-magicmirror2
|
||||
Version: 0.4.0-r8
|
||||
@ -860,7 +860,7 @@ Description: MagicMirror² - Open source modular smart mirror platform for Secu
|
||||
Runs in LXC container for isolation and security.
|
||||
Configure in /etc/config/magicmirror2.
|
||||
Filename: secubox-app-magicmirror2_0.4.0-r8_all.ipk
|
||||
Size: 9251
|
||||
Size: 9249
|
||||
|
||||
Package: secubox-app-mailinabox
|
||||
Version: 2.0.0-r1
|
||||
@ -885,7 +885,7 @@ Description: Complete email server solution using docker-mailserver for SecuBox
|
||||
|
||||
Commands: mailinaboxctl --help
|
||||
Filename: secubox-app-mailinabox_2.0.0-r1_all.ipk
|
||||
Size: 7566
|
||||
Size: 7575
|
||||
|
||||
Package: secubox-app-metabolizer
|
||||
Version: 1.0.0-r3
|
||||
@ -906,7 +906,7 @@ Description: Metabolizer Blog Pipeline - Integrated CMS with Git-based workflow
|
||||
|
||||
Pipeline: Edit in Streamlit -> Push to Gitea -> Build with Hexo -> Publish
|
||||
Filename: secubox-app-metabolizer_1.0.0-r3_all.ipk
|
||||
Size: 13979
|
||||
Size: 13984
|
||||
|
||||
Package: secubox-app-mitmproxy
|
||||
Version: 0.4.0-r16
|
||||
@ -927,7 +927,7 @@ Description: mitmproxy - Interactive HTTPS proxy for SecuBox-powered OpenWrt sy
|
||||
Runs in LXC container for isolation and security.
|
||||
Configure in /etc/config/mitmproxy.
|
||||
Filename: secubox-app-mitmproxy_0.4.0-r16_all.ipk
|
||||
Size: 10215
|
||||
Size: 10213
|
||||
|
||||
Package: secubox-app-mmpm
|
||||
Version: 0.2.0-r5
|
||||
@ -948,7 +948,7 @@ Description: MMPM (MagicMirror Package Manager) for SecuBox.
|
||||
|
||||
Runs inside the MagicMirror2 LXC container.
|
||||
Filename: secubox-app-mmpm_0.2.0-r5_all.ipk
|
||||
Size: 3974
|
||||
Size: 3977
|
||||
|
||||
Package: secubox-app-nextcloud
|
||||
Version: 1.0.0-r2
|
||||
@ -962,7 +962,7 @@ Description: Installer, configuration, and service manager for running Nextclou
|
||||
inside Docker on SecuBox-powered OpenWrt systems. Self-hosted file
|
||||
sync and share with calendar, contacts, and collaboration.
|
||||
Filename: secubox-app-nextcloud_1.0.0-r2_all.ipk
|
||||
Size: 2955
|
||||
Size: 2959
|
||||
|
||||
Package: secubox-app-ollama
|
||||
Version: 0.1.0-r1
|
||||
@ -984,7 +984,7 @@ Description: Ollama - Simple local LLM runtime for SecuBox-powered OpenWrt syst
|
||||
Runs in Docker/Podman container.
|
||||
Configure in /etc/config/ollama.
|
||||
Filename: secubox-app-ollama_0.1.0-r1_all.ipk
|
||||
Size: 5733
|
||||
Size: 5739
|
||||
|
||||
Package: secubox-app-picobrew
|
||||
Version: 1.0.0-r7
|
||||
@ -1033,7 +1033,7 @@ Description: Streamlit App Platform - Self-hosted Python data app platform
|
||||
|
||||
Configure in /etc/config/streamlit.
|
||||
Filename: secubox-app-streamlit_1.0.0-r5_all.ipk
|
||||
Size: 11717
|
||||
Size: 11721
|
||||
|
||||
Package: secubox-app-tor
|
||||
Version: 1.0.0-r1
|
||||
@ -1056,7 +1056,7 @@ Description: SecuBox Tor Shield - One-click Tor anonymization for OpenWrt
|
||||
|
||||
Configure in /etc/config/tor-shield.
|
||||
Filename: secubox-app-tor_1.0.0-r1_all.ipk
|
||||
Size: 7379
|
||||
Size: 7377
|
||||
|
||||
Package: secubox-app-webapp
|
||||
Version: 1.5.0-r7
|
||||
@ -1074,7 +1074,7 @@ Description: SecuBox Control Center Dashboard - A web-based dashboard for monit
|
||||
- Service management
|
||||
- Network interface control
|
||||
Filename: secubox-app-webapp_1.5.0-r7_all.ipk
|
||||
Size: 39169
|
||||
Size: 39171
|
||||
|
||||
Package: secubox-app-zigbee2mqtt
|
||||
Version: 1.0.0-r3
|
||||
@ -1087,7 +1087,7 @@ Installed-Size: 20480
|
||||
Description: Installer, configuration, and service manager for running Zigbee2MQTT
|
||||
inside Docker on SecuBox-powered OpenWrt systems.
|
||||
Filename: secubox-app-zigbee2mqtt_1.0.0-r3_all.ipk
|
||||
Size: 3542
|
||||
Size: 3545
|
||||
|
||||
Package: secubox-core
|
||||
Version: 0.10.0-r9
|
||||
@ -1107,21 +1107,22 @@ Description: SecuBox Core Framework provides the foundational infrastructure fo
|
||||
- Unified CLI interface
|
||||
- ubus RPC backend
|
||||
Filename: secubox-core_0.10.0-r9_all.ipk
|
||||
Size: 80068
|
||||
Size: 80071
|
||||
|
||||
Package: secubox-p2p
|
||||
Version: 0.4.0-r1
|
||||
Version: 0.5.0-r1
|
||||
Depends: jsonfilter, curl, avahi-daemon, avahi-utils, uhttpd
|
||||
License: MIT
|
||||
Section: secubox
|
||||
Maintainer: SecuBox Team
|
||||
Architecture: all
|
||||
Installed-Size: 133120
|
||||
Installed-Size: 143360
|
||||
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.
|
||||
Filename: secubox-p2p_0.4.0-r1_all.ipk
|
||||
Size: 27891
|
||||
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.
|
||||
Filename: secubox-p2p_0.5.0-r1_all.ipk
|
||||
Size: 30574
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-p2p
|
||||
PKG_VERSION:=0.5.0
|
||||
PKG_VERSION:=0.6.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=SecuBox Team
|
||||
@ -22,8 +22,10 @@ define Package/secubox-p2p/description
|
||||
DNS federation, and distributed service management. Includes mDNS
|
||||
service announcement, REST API on port 7331 for mesh visibility,
|
||||
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.
|
||||
snapshots for cryptographic configuration validation, distributed
|
||||
mesh services panel for aggregated service discovery across all nodes,
|
||||
and MirrorBox NetMesh Catalog for cross-chain distributed service
|
||||
registry with HAProxy vhost discovery and multi-endpoint access URLs.
|
||||
endef
|
||||
|
||||
define Package/secubox-p2p/conffiles
|
||||
@ -66,6 +68,7 @@ define Package/secubox-p2p/install
|
||||
$(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/
|
||||
$(INSTALL_BIN) ./root/www/api/factory/catalog $(1)/www/api/factory/
|
||||
|
||||
# Factory Web UI
|
||||
$(INSTALL_DIR) $(1)/www/factory
|
||||
|
||||
@ -295,6 +295,177 @@ factory_audit_log() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ===========================================
|
||||
# Distributed Catalog Functions
|
||||
# ===========================================
|
||||
|
||||
CATALOG_DIR="$FACTORY_DIR/catalog"
|
||||
LOCAL_CATALOG="$CATALOG_DIR/local.json"
|
||||
MERGED_CATALOG="$CATALOG_DIR/merged.json"
|
||||
|
||||
# Initialize catalog directory
|
||||
catalog_init() {
|
||||
factory_init
|
||||
mkdir -p "$CATALOG_DIR"
|
||||
mkdir -p "$CATALOG_DIR/peers"
|
||||
}
|
||||
|
||||
# Generate local catalog (calls the CGI endpoint logic)
|
||||
catalog_generate_local() {
|
||||
catalog_init
|
||||
local node_id=$(cat "$P2P_STATE_DIR/node.id" 2>/dev/null || hostname)
|
||||
local node_name=$(uci -q get system.@system[0].hostname || hostname)
|
||||
local updated=$(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
# Fetch from local catalog API
|
||||
local catalog=$(curl -s "http://127.0.0.1:7331/api/factory/catalog" 2>/dev/null)
|
||||
|
||||
if [ -n "$catalog" ] && echo "$catalog" | grep -q "node_id"; then
|
||||
echo "$catalog" > "$LOCAL_CATALOG"
|
||||
echo "$LOCAL_CATALOG"
|
||||
else
|
||||
echo '{"error":"catalog_generation_failed"}' > "$LOCAL_CATALOG"
|
||||
echo "$LOCAL_CATALOG"
|
||||
fi
|
||||
}
|
||||
|
||||
# Pull catalog from a peer
|
||||
catalog_pull_peer() {
|
||||
local peer_addr="$1"
|
||||
local peer_name="${2:-$peer_addr}"
|
||||
|
||||
[ -z "$peer_addr" ] && return 1
|
||||
|
||||
catalog_init
|
||||
|
||||
local peer_catalog=$(curl -s --connect-timeout 3 --max-time 10 "http://$peer_addr:7331/api/factory/catalog" 2>/dev/null)
|
||||
|
||||
if [ -n "$peer_catalog" ] && echo "$peer_catalog" | grep -q "node_id"; then
|
||||
local peer_node_name=$(echo "$peer_catalog" | jsonfilter -e '@.node_name' 2>/dev/null || echo "$peer_name")
|
||||
echo "$peer_catalog" > "$CATALOG_DIR/peers/${peer_node_name}.json"
|
||||
echo "pulled:$peer_node_name"
|
||||
else
|
||||
echo "failed:$peer_addr"
|
||||
fi
|
||||
}
|
||||
|
||||
# Sync catalogs with all known peers
|
||||
catalog_sync() {
|
||||
catalog_init
|
||||
|
||||
local peers_file="/tmp/secubox-p2p-peers.json"
|
||||
[ -f "$peers_file" ] || { echo '{"synced":0,"error":"no_peers"}'; return 1; }
|
||||
|
||||
# Generate local catalog first
|
||||
catalog_generate_local >/dev/null 2>&1
|
||||
|
||||
# Pull from all online peers
|
||||
local synced=0
|
||||
local failed=0
|
||||
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 is_local=$(jsonfilter -i "$peers_file" -e "@.peers[$p].is_local" 2>/dev/null)
|
||||
|
||||
if [ "$is_local" != "true" ] && [ -n "$peer_addr" ]; then
|
||||
local result=$(catalog_pull_peer "$peer_addr" "$peer_name")
|
||||
if echo "$result" | grep -q "^pulled:"; then
|
||||
synced=$((synced + 1))
|
||||
else
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
fi
|
||||
p=$((p + 1))
|
||||
done
|
||||
|
||||
# Merge all catalogs
|
||||
catalog_merge
|
||||
|
||||
# Push to Gitea if enabled
|
||||
if [ "$(uci -q get secubox-p2p.gitea.enabled)" = "1" ]; then
|
||||
catalog_push_gitea
|
||||
fi
|
||||
|
||||
echo "{\"synced\":$synced,\"failed\":$failed}"
|
||||
}
|
||||
|
||||
# Merge all catalogs into unified view (CRDT union)
|
||||
catalog_merge() {
|
||||
catalog_init
|
||||
|
||||
local merged_services='{"nodes":['
|
||||
local first_node=1
|
||||
|
||||
# Add local catalog
|
||||
if [ -f "$LOCAL_CATALOG" ]; then
|
||||
[ $first_node -eq 0 ] && merged_services="$merged_services,"
|
||||
first_node=0
|
||||
cat "$LOCAL_CATALOG" | tr '\n' ' ' | tr '\t' ' ' >> /tmp/catalog_merge_local.tmp
|
||||
local local_entry=$(cat /tmp/catalog_merge_local.tmp)
|
||||
merged_services="$merged_services$local_entry"
|
||||
rm -f /tmp/catalog_merge_local.tmp
|
||||
fi
|
||||
|
||||
# Add peer catalogs
|
||||
for peer_catalog in "$CATALOG_DIR/peers"/*.json; do
|
||||
[ -f "$peer_catalog" ] || continue
|
||||
[ $first_node -eq 0 ] && merged_services="$merged_services,"
|
||||
first_node=0
|
||||
cat "$peer_catalog" | tr '\n' ' ' | tr '\t' ' ' >> /tmp/catalog_merge_peer.tmp
|
||||
local peer_entry=$(cat /tmp/catalog_merge_peer.tmp)
|
||||
merged_services="$merged_services$peer_entry"
|
||||
rm -f /tmp/catalog_merge_peer.tmp
|
||||
done
|
||||
|
||||
merged_services="$merged_services],\"updated\":\"$(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S')\"}"
|
||||
|
||||
echo "$merged_services" > "$MERGED_CATALOG"
|
||||
}
|
||||
|
||||
# Push catalog to Gitea repository
|
||||
catalog_push_gitea() {
|
||||
local gitea_enabled=$(uci -q get secubox-p2p.gitea.enabled)
|
||||
[ "$gitea_enabled" = "1" ] || return 0
|
||||
|
||||
local node_name=$(uci -q get system.@system[0].hostname || hostname)
|
||||
|
||||
# This would push to secubox-catalog repo in Gitea
|
||||
# For now, just log the action
|
||||
logger -t factory "Catalog sync: would push to Gitea nodes/${node_name}.json"
|
||||
|
||||
# Future: use ubus call to trigger actual git push
|
||||
# ubus call luci.secubox-p2p push_catalog_gitea "{\"file\":\"nodes/${node_name}.json\"}" 2>/dev/null
|
||||
}
|
||||
|
||||
# Get merged catalog JSON
|
||||
catalog_get_merged() {
|
||||
if [ -f "$MERGED_CATALOG" ]; then
|
||||
cat "$MERGED_CATALOG"
|
||||
elif [ -f "$LOCAL_CATALOG" ]; then
|
||||
# Return local only if no merge yet
|
||||
echo "{\"nodes\":[$(cat "$LOCAL_CATALOG" | tr '\n' ' ')],\"updated\":\"$(date -Iseconds)\"}"
|
||||
else
|
||||
echo '{"nodes":[],"updated":"","error":"no_catalog"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# List available catalogs
|
||||
catalog_list() {
|
||||
catalog_init
|
||||
echo "Local catalog: $LOCAL_CATALOG"
|
||||
[ -f "$LOCAL_CATALOG" ] && echo " - $(jsonfilter -i "$LOCAL_CATALOG" -e '@.node_name' 2>/dev/null || echo 'unknown')"
|
||||
|
||||
echo "Peer catalogs:"
|
||||
for peer_catalog in "$CATALOG_DIR/peers"/*.json; do
|
||||
[ -f "$peer_catalog" ] || continue
|
||||
local name=$(basename "$peer_catalog" .json)
|
||||
echo " - $name"
|
||||
done
|
||||
}
|
||||
|
||||
# Main entry point for CLI usage
|
||||
case "${1:-}" in
|
||||
init)
|
||||
@ -331,6 +502,33 @@ case "${1:-}" in
|
||||
merkle)
|
||||
merkle_config
|
||||
;;
|
||||
# Catalog commands
|
||||
catalog)
|
||||
case "${2:-}" in
|
||||
sync)
|
||||
catalog_sync
|
||||
;;
|
||||
list)
|
||||
catalog_list
|
||||
;;
|
||||
generate)
|
||||
catalog_generate_local
|
||||
;;
|
||||
merge)
|
||||
catalog_merge
|
||||
echo "Merged catalog: $MERGED_CATALOG"
|
||||
;;
|
||||
get)
|
||||
catalog_get_merged
|
||||
;;
|
||||
pull)
|
||||
catalog_pull_peer "$3" "$4"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: factory.sh catalog {sync|list|generate|merge|get|pull <peer_addr>}"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
# Sourced as library - do nothing
|
||||
:
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
# Handles peer discovery, mesh networking, and service federation
|
||||
# Supports WAN IP, LAN IP, and WireGuard tunnel redundancy
|
||||
|
||||
VERSION="0.4.0"
|
||||
VERSION="0.6.0"
|
||||
CONFIG_FILE="/etc/config/secubox-p2p"
|
||||
PEERS_FILE="/tmp/secubox-p2p-peers.json"
|
||||
SERVICES_FILE="/tmp/secubox-p2p-services.json"
|
||||
@ -439,6 +439,210 @@ stop_mdns() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ===========================================
|
||||
# DNS Federation - Dynamic mesh DNS entries
|
||||
# ===========================================
|
||||
|
||||
DNS_HOSTS_FILE="/tmp/hosts/secubox-mesh"
|
||||
DNS_DOMAIN_BASE="mesh.local"
|
||||
|
||||
# Initialize DNS federation
|
||||
dns_init() {
|
||||
local dns_enabled=$(get_config dns enabled 0)
|
||||
[ "$dns_enabled" = "1" ] || return 0
|
||||
|
||||
DNS_DOMAIN_BASE=$(get_config dns base_domain "mesh.local")
|
||||
|
||||
# Ensure hosts directory exists
|
||||
mkdir -p /tmp/hosts
|
||||
|
||||
# Create or update mesh hosts file
|
||||
touch "$DNS_HOSTS_FILE"
|
||||
|
||||
# Ensure dnsmasq reads from /tmp/hosts
|
||||
# Check if addn-hosts is configured
|
||||
if ! uci -q get dhcp.@dnsmasq[0].addnhosts | grep -q "/tmp/hosts"; then
|
||||
uci add_list dhcp.@dnsmasq[0].addnhosts="/tmp/hosts"
|
||||
uci commit dhcp
|
||||
logger -t secubox-p2p "Added /tmp/hosts to dnsmasq additional hosts"
|
||||
fi
|
||||
}
|
||||
|
||||
# Update DNS entries for all mesh peers
|
||||
dns_update_peers() {
|
||||
local dns_enabled=$(get_config dns enabled 0)
|
||||
[ "$dns_enabled" = "1" ] || return 0
|
||||
|
||||
dns_init
|
||||
|
||||
local base_domain=$(get_config dns base_domain "mesh.local")
|
||||
local hosts_content=""
|
||||
local count=0
|
||||
|
||||
# Get all peers (including local)
|
||||
local peers_json=$(cat "$PEERS_FILE" 2>/dev/null || echo '{"peers":[]}')
|
||||
|
||||
# Parse peers and build hosts entries
|
||||
# Format: IP hostname hostname.mesh.local
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
hosts_content=$(echo "$peers_json" | jq -r '.peers[] | select(.status == "online" or .is_local == true) |
|
||||
# Build hostname entries
|
||||
(
|
||||
(.address // empty) as $lan |
|
||||
(.wan_address // empty) as $wan |
|
||||
(.wg_addresses // empty) as $wg |
|
||||
(.name // "peer") | gsub(" \\(local\\)"; "") | gsub("[^a-zA-Z0-9-]"; "-") | ascii_downcase as $hostname |
|
||||
|
||||
# LAN entry (primary for local network)
|
||||
(if $lan != "" and $lan != null then
|
||||
"\($lan)\t\($hostname)\t\($hostname).'$base_domain'"
|
||||
else empty end),
|
||||
|
||||
# WireGuard entry (for mesh access)
|
||||
(if $wg != "" and $wg != null then
|
||||
($wg | split(",")[0]) as $wg_ip |
|
||||
if $wg_ip != "" then
|
||||
"\($wg_ip)\t\($hostname)-wg\t\($hostname)-wg.'$base_domain'"
|
||||
else empty end
|
||||
else empty end)
|
||||
)
|
||||
' 2>/dev/null)
|
||||
else
|
||||
# Fallback without jq - use jsonfilter
|
||||
local peer_count=$(echo "$peers_json" | jsonfilter -e '@.peers[*]' 2>/dev/null | wc -l)
|
||||
local i=0
|
||||
|
||||
while [ $i -lt $peer_count ]; do
|
||||
local addr=$(echo "$peers_json" | jsonfilter -e "@.peers[$i].address" 2>/dev/null)
|
||||
local name=$(echo "$peers_json" | jsonfilter -e "@.peers[$i].name" 2>/dev/null)
|
||||
local status=$(echo "$peers_json" | jsonfilter -e "@.peers[$i].status" 2>/dev/null)
|
||||
local is_local=$(echo "$peers_json" | jsonfilter -e "@.peers[$i].is_local" 2>/dev/null)
|
||||
local wg_addr=$(echo "$peers_json" | jsonfilter -e "@.peers[$i].wg_addresses" 2>/dev/null | cut -d',' -f1)
|
||||
|
||||
# Only add online peers or local node
|
||||
if [ "$status" = "online" ] || [ "$is_local" = "true" ]; then
|
||||
# Clean hostname (remove special chars)
|
||||
local hostname=$(echo "$name" | sed -e 's/ (local)//' -e 's/[^a-zA-Z0-9-]/-/g' | tr 'A-Z' 'a-z')
|
||||
|
||||
# Add LAN entry
|
||||
if [ -n "$addr" ]; then
|
||||
hosts_content="$hosts_content$addr $hostname $hostname.$base_domain
|
||||
"
|
||||
count=$((count + 1))
|
||||
fi
|
||||
|
||||
# Add WireGuard entry
|
||||
if [ -n "$wg_addr" ]; then
|
||||
hosts_content="$hosts_content$wg_addr ${hostname}-wg ${hostname}-wg.$base_domain
|
||||
"
|
||||
count=$((count + 1))
|
||||
fi
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
|
||||
# Write hosts file
|
||||
echo "# SecuBox Mesh DNS Federation - Auto-generated" > "$DNS_HOSTS_FILE"
|
||||
echo "# Domain: $base_domain" >> "$DNS_HOSTS_FILE"
|
||||
echo "# Updated: $(date -Iseconds 2>/dev/null || date)" >> "$DNS_HOSTS_FILE"
|
||||
echo "" >> "$DNS_HOSTS_FILE"
|
||||
echo "$hosts_content" >> "$DNS_HOSTS_FILE"
|
||||
|
||||
# Count actual entries (non-comment, non-empty lines)
|
||||
count=$(grep -c "^[0-9]" "$DNS_HOSTS_FILE" 2>/dev/null || echo "0")
|
||||
|
||||
# Signal dnsmasq to reload hosts
|
||||
killall -HUP dnsmasq 2>/dev/null || /etc/init.d/dnsmasq reload 2>/dev/null
|
||||
|
||||
logger -t secubox-p2p "DNS federation updated: $count mesh host entries in $base_domain"
|
||||
echo "{\"success\":true,\"entries\":$count,\"domain\":\"$base_domain\"}"
|
||||
}
|
||||
|
||||
# Add a single peer to DNS
|
||||
dns_add_peer() {
|
||||
local hostname="$1"
|
||||
local ip="$2"
|
||||
|
||||
local dns_enabled=$(get_config dns enabled 0)
|
||||
[ "$dns_enabled" = "1" ] || return 0
|
||||
|
||||
local base_domain=$(get_config dns base_domain "mesh.local")
|
||||
|
||||
# Clean hostname
|
||||
hostname=$(echo "$hostname" | sed -e 's/[^a-zA-Z0-9-]/-/g' | tr 'A-Z' 'a-z')
|
||||
|
||||
# Append to hosts file (avoid duplicates)
|
||||
if ! grep -q " $hostname " "$DNS_HOSTS_FILE" 2>/dev/null; then
|
||||
echo "$ip $hostname $hostname.$base_domain" >> "$DNS_HOSTS_FILE"
|
||||
killall -HUP dnsmasq 2>/dev/null
|
||||
logger -t secubox-p2p "DNS: Added $hostname.$base_domain -> $ip"
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove a peer from DNS
|
||||
dns_remove_peer() {
|
||||
local hostname="$1"
|
||||
|
||||
local dns_enabled=$(get_config dns enabled 0)
|
||||
[ "$dns_enabled" = "1" ] || return 0
|
||||
|
||||
# Clean hostname
|
||||
hostname=$(echo "$hostname" | sed -e 's/[^a-zA-Z0-9-]/-/g' | tr 'A-Z' 'a-z')
|
||||
|
||||
# Remove from hosts file
|
||||
sed -i "/ $hostname /d" "$DNS_HOSTS_FILE" 2>/dev/null
|
||||
killall -HUP dnsmasq 2>/dev/null
|
||||
logger -t secubox-p2p "DNS: Removed $hostname"
|
||||
}
|
||||
|
||||
# Get DNS federation status
|
||||
dns_status() {
|
||||
local dns_enabled=$(get_config dns enabled 0)
|
||||
local base_domain=$(get_config dns base_domain "mesh.local")
|
||||
local entry_count=0
|
||||
|
||||
if [ -f "$DNS_HOSTS_FILE" ]; then
|
||||
entry_count=$(grep -c "^[0-9]" "$DNS_HOSTS_FILE" 2>/dev/null || echo "0")
|
||||
fi
|
||||
|
||||
cat << EOF
|
||||
{
|
||||
"enabled": $dns_enabled,
|
||||
"domain": "$base_domain",
|
||||
"hosts_file": "$DNS_HOSTS_FILE",
|
||||
"entries": $entry_count,
|
||||
"dnsmasq_running": $(pgrep dnsmasq >/dev/null && echo "true" || echo "false")
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Enable DNS federation
|
||||
dns_enable() {
|
||||
set_config dns enabled 1
|
||||
|
||||
local base_domain="${1:-mesh.local}"
|
||||
set_config dns base_domain "$base_domain"
|
||||
|
||||
dns_init
|
||||
dns_update_peers
|
||||
|
||||
logger -t secubox-p2p "DNS federation enabled with domain: $base_domain"
|
||||
echo "{\"success\":true,\"domain\":\"$base_domain\"}"
|
||||
}
|
||||
|
||||
# Disable DNS federation
|
||||
dns_disable() {
|
||||
set_config dns enabled 0
|
||||
|
||||
# Clear hosts file
|
||||
echo "# SecuBox Mesh DNS Federation - Disabled" > "$DNS_HOSTS_FILE"
|
||||
killall -HUP dnsmasq 2>/dev/null
|
||||
|
||||
logger -t secubox-p2p "DNS federation disabled"
|
||||
echo "{\"success\":true}"
|
||||
}
|
||||
|
||||
# Get node status JSON (for REST API)
|
||||
# Now includes WAN IP and WireGuard tunnel addresses
|
||||
get_node_status() {
|
||||
@ -526,6 +730,9 @@ daemon_loop() {
|
||||
# Register self in peer list
|
||||
register_self
|
||||
|
||||
# Initialize DNS federation
|
||||
dns_init
|
||||
|
||||
# Setup signal handlers
|
||||
trap 'stop_mdns; exit 0' INT TERM
|
||||
|
||||
@ -603,6 +810,9 @@ daemon_loop() {
|
||||
echo "$peers" > "$PEERS_FILE"
|
||||
fi
|
||||
|
||||
# Update DNS federation with current peer list
|
||||
dns_update_peers >/dev/null 2>&1
|
||||
|
||||
# Sleep interval
|
||||
local interval=$(get_config main sync_interval 60)
|
||||
sleep "$interval"
|
||||
@ -660,8 +870,28 @@ case "$1" in
|
||||
init
|
||||
register_self
|
||||
;;
|
||||
# DNS Federation commands
|
||||
dns-status)
|
||||
dns_status
|
||||
;;
|
||||
dns-enable)
|
||||
dns_enable "$2"
|
||||
;;
|
||||
dns-disable)
|
||||
dns_disable
|
||||
;;
|
||||
dns-update)
|
||||
init
|
||||
dns_update_peers
|
||||
;;
|
||||
dns-add)
|
||||
dns_add_peer "$2" "$3"
|
||||
;;
|
||||
dns-remove)
|
||||
dns_remove_peer "$2"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {daemon|discover|peers|add-peer|remove-peer|settings|set-settings|services|shared-services|sync|broadcast|version|status|publish-mdns|stop-mdns|register-self}"
|
||||
echo "Usage: $0 {daemon|discover|peers|add-peer|remove-peer|settings|set-settings|services|shared-services|sync|broadcast|version|status|publish-mdns|stop-mdns|register-self|dns-status|dns-enable|dns-disable|dns-update|dns-add|dns-remove}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
351
package/secubox/secubox-p2p/root/www/api/factory/catalog
Normal file
351
package/secubox/secubox-p2p/root/www/api/factory/catalog
Normal file
@ -0,0 +1,351 @@
|
||||
#!/bin/sh
|
||||
# Factory Catalog - Distributed Service Registry
|
||||
# CGI endpoint for MirrorBox NetMesh Catalog
|
||||
# Returns services with multiple access endpoints (HAProxy vhosts, mesh, local)
|
||||
|
||||
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
|
||||
|
||||
# Source OpenWrt functions for UCI iteration
|
||||
. /lib/functions.sh
|
||||
|
||||
# Load factory library
|
||||
. /usr/lib/secubox/factory.sh 2>/dev/null
|
||||
|
||||
# Get local node info
|
||||
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)
|
||||
LOCAL_IP=$(uci -q get network.lan.ipaddr || echo "192.168.255.1")
|
||||
UPDATED=$(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
# DNS Federation settings
|
||||
DNS_ENABLED=$(uci -q get secubox-p2p.dns.enabled || echo "0")
|
||||
DNS_DOMAIN=$(uci -q get secubox-p2p.dns.base_domain || echo "mesh.local")
|
||||
|
||||
# Generate DNS hostname from node name
|
||||
get_dns_hostname() {
|
||||
local name="$1"
|
||||
# Remove "(local)" suffix, lowercase, replace special chars with dashes
|
||||
echo "$name" | sed -e 's/ (local)//' -e 's/[^a-zA-Z0-9-]/-/g' | tr 'A-Z' 'a-z'
|
||||
}
|
||||
|
||||
# Get WAN IP (for external access)
|
||||
get_wan_ip() {
|
||||
# Try to get WAN IP from interface
|
||||
local wan_ip=$(ip -4 addr show wan 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
|
||||
[ -z "$wan_ip" ] && wan_ip=$(ip -4 addr show pppoe-wan 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
|
||||
[ -z "$wan_ip" ] && wan_ip=$(ip -4 addr show eth1 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
|
||||
# Fallback: try to detect via external service (cached)
|
||||
if [ -z "$wan_ip" ] && [ -f "/tmp/secubox-wan-ip" ]; then
|
||||
wan_ip=$(cat /tmp/secubox-wan-ip)
|
||||
fi
|
||||
echo "$wan_ip"
|
||||
}
|
||||
|
||||
# Get WireGuard mesh IP
|
||||
get_mesh_ip() {
|
||||
local mesh_ip=$(ip -4 addr show wg0 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
|
||||
echo "$mesh_ip"
|
||||
}
|
||||
|
||||
# Build HAProxy vhost map: domain -> backend
|
||||
# Returns: domain|backend|ssl|enabled for each vhost
|
||||
get_haproxy_vhosts() {
|
||||
config_load haproxy 2>/dev/null || return
|
||||
|
||||
# Callback function for each vhost
|
||||
_emit_vhost() {
|
||||
local section="$1"
|
||||
local enabled domain backend ssl ssl_redirect
|
||||
|
||||
config_get enabled "$section" enabled "0"
|
||||
[ "$enabled" = "1" ] || return
|
||||
|
||||
config_get domain "$section" domain
|
||||
config_get backend "$section" backend
|
||||
config_get ssl "$section" ssl "0"
|
||||
config_get ssl_redirect "$section" ssl_redirect "0"
|
||||
|
||||
[ -n "$domain" ] || return
|
||||
[ -n "$backend" ] || return
|
||||
|
||||
# Determine scheme
|
||||
local scheme="http"
|
||||
[ "$ssl" = "1" ] && scheme="https"
|
||||
|
||||
echo "${domain}|${backend}|${scheme}|${ssl_redirect}"
|
||||
}
|
||||
|
||||
config_foreach _emit_vhost vhost
|
||||
}
|
||||
|
||||
# Map backend name to service name (normalize)
|
||||
# E.g., backend_gitea -> gitea, srv_domoticz -> domoticz
|
||||
normalize_backend_name() {
|
||||
local backend="$1"
|
||||
# Strip common prefixes
|
||||
echo "$backend" | sed -e 's/^backend_//' -e 's/^srv_//' -e 's/^be_//' -e 's/_/-/g'
|
||||
}
|
||||
|
||||
# Get service status and port from init scripts
|
||||
get_service_info() {
|
||||
local svc_name="$1"
|
||||
local status="unknown"
|
||||
local port=""
|
||||
local enabled=false
|
||||
|
||||
# Check init script
|
||||
local init_script="/etc/init.d/$svc_name"
|
||||
if [ -x "$init_script" ]; then
|
||||
enabled=$("$init_script" enabled 2>/dev/null && echo true || echo false)
|
||||
# Check if running
|
||||
if pgrep "$svc_name" >/dev/null 2>&1; then
|
||||
status="running"
|
||||
else
|
||||
status="stopped"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try to find port from UCI or known mappings
|
||||
case "$svc_name" in
|
||||
gitea) port="3000" ;;
|
||||
domoticz) port="8080" ;;
|
||||
crowdsec) port="8080" ;;
|
||||
haproxy) port="8404" ;;
|
||||
adguardhome) port="3000" ;;
|
||||
netdata) port="19999" ;;
|
||||
glances) port="61208" ;;
|
||||
streamlit) port="8501" ;;
|
||||
zigbee2mqtt) port="8080" ;;
|
||||
nextcloud) port="8080" ;;
|
||||
ollama) port="11434" ;;
|
||||
localai) port="8080" ;;
|
||||
wireguard-dashboard|wg-dashboard) port="10086" ;;
|
||||
mitmproxy) port="8081" ;;
|
||||
*)
|
||||
# Try to get from UCI config
|
||||
port=$(uci -q get "$svc_name.main.port" 2>/dev/null)
|
||||
[ -z "$port" ] && port=$(uci -q get "$svc_name.@server[0].port" 2>/dev/null)
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$status|$port|$enabled"
|
||||
}
|
||||
|
||||
# Build catalog entries from HAProxy vhosts and local services
|
||||
build_catalog() {
|
||||
local wan_ip=$(get_wan_ip)
|
||||
local mesh_ip=$(get_mesh_ip)
|
||||
local first_service=1
|
||||
|
||||
# Start services array
|
||||
echo '"services": ['
|
||||
|
||||
# Track processed backends to avoid duplicates
|
||||
local processed_backends=""
|
||||
|
||||
# First, enumerate HAProxy vhosts (primary access method)
|
||||
local vhosts=$(get_haproxy_vhosts)
|
||||
for vhost_line in $vhosts; do
|
||||
local domain=$(echo "$vhost_line" | cut -d'|' -f1)
|
||||
local backend=$(echo "$vhost_line" | cut -d'|' -f2)
|
||||
local scheme=$(echo "$vhost_line" | cut -d'|' -f3)
|
||||
local ssl_redirect=$(echo "$vhost_line" | cut -d'|' -f4)
|
||||
|
||||
local svc_name=$(normalize_backend_name "$backend")
|
||||
|
||||
# Skip if already processed
|
||||
echo "$processed_backends" | grep -qw "$svc_name" && continue
|
||||
processed_backends="$processed_backends $svc_name"
|
||||
|
||||
# Get service status
|
||||
local svc_info=$(get_service_info "$svc_name")
|
||||
local status=$(echo "$svc_info" | cut -d'|' -f1)
|
||||
local port=$(echo "$svc_info" | cut -d'|' -f2)
|
||||
local enabled=$(echo "$svc_info" | cut -d'|' -f3)
|
||||
|
||||
[ $first_service -eq 0 ] && echo ","
|
||||
first_service=0
|
||||
|
||||
# Build endpoints array
|
||||
local endpoints='['
|
||||
local first_ep=1
|
||||
|
||||
# Primary: HAProxy vhost (published domain)
|
||||
[ $first_ep -eq 0 ] && endpoints="$endpoints,"
|
||||
first_ep=0
|
||||
endpoints="$endpoints{\"type\":\"haproxy\",\"url\":\"${scheme}://${domain}\",\"ssl\":$([ \"$scheme\" = \"https\" ] && echo true || echo false),\"primary\":true}"
|
||||
|
||||
# Collect all vhosts for this backend
|
||||
for other_vhost in $vhosts; do
|
||||
local other_domain=$(echo "$other_vhost" | cut -d'|' -f1)
|
||||
local other_backend=$(echo "$other_vhost" | cut -d'|' -f2)
|
||||
local other_scheme=$(echo "$other_vhost" | cut -d'|' -f3)
|
||||
|
||||
[ "$other_backend" = "$backend" ] || continue
|
||||
[ "$other_domain" = "$domain" ] && continue # Skip primary
|
||||
|
||||
endpoints="$endpoints,{\"type\":\"haproxy\",\"url\":\"${other_scheme}://${other_domain}\",\"ssl\":$([ \"$other_scheme\" = \"https\" ] && echo true || echo false),\"primary\":false}"
|
||||
done
|
||||
|
||||
# Mesh endpoint (WireGuard)
|
||||
if [ -n "$mesh_ip" ] && [ -n "$port" ]; then
|
||||
endpoints="$endpoints,{\"type\":\"mesh\",\"url\":\"http://${mesh_ip}:${port}\",\"ssl\":false,\"primary\":false}"
|
||||
fi
|
||||
|
||||
# Local endpoint (LAN)
|
||||
if [ -n "$port" ]; then
|
||||
endpoints="$endpoints,{\"type\":\"local\",\"url\":\"http://${LOCAL_IP}:${port}\",\"ssl\":false,\"primary\":false}"
|
||||
fi
|
||||
|
||||
endpoints="$endpoints]"
|
||||
|
||||
# Output service entry
|
||||
cat << SVCEOF
|
||||
{
|
||||
"name": "$svc_name",
|
||||
"backend": "$backend",
|
||||
"status": "$status",
|
||||
"enabled": $enabled,
|
||||
"port": "${port:-}",
|
||||
"endpoints": $endpoints,
|
||||
"health": {
|
||||
"last_check": "$UPDATED",
|
||||
"status": "$([ \"$status\" = \"running\" ] && echo healthy || echo unhealthy)",
|
||||
"latency_ms": 0
|
||||
}
|
||||
}
|
||||
SVCEOF
|
||||
done
|
||||
|
||||
# Now add services without HAProxy vhosts (mesh/local only)
|
||||
local services_json=$(/usr/sbin/secubox-p2p services 2>/dev/null)
|
||||
if [ -n "$services_json" ]; then
|
||||
local svc_count=$(echo "$services_json" | jsonfilter -e '@.services[*]' 2>/dev/null | wc -l)
|
||||
local i=0
|
||||
|
||||
while [ $i -lt $svc_count ]; do
|
||||
local svc_name=$(echo "$services_json" | jsonfilter -e "@.services[$i].name" 2>/dev/null)
|
||||
local svc_status=$(echo "$services_json" | jsonfilter -e "@.services[$i].status" 2>/dev/null)
|
||||
local svc_enabled=$(echo "$services_json" | jsonfilter -e "@.services[$i].enabled" 2>/dev/null)
|
||||
local svc_port=$(echo "$services_json" | jsonfilter -e "@.services[$i].port" 2>/dev/null)
|
||||
|
||||
# Skip if already processed via HAProxy
|
||||
if ! echo "$processed_backends" | grep -qw "$svc_name"; then
|
||||
[ $first_service -eq 0 ] && echo ","
|
||||
first_service=0
|
||||
|
||||
# Build endpoints (mesh + local only, no HAProxy)
|
||||
local endpoints='['
|
||||
local first_ep=1
|
||||
|
||||
# Mesh endpoint
|
||||
if [ -n "$mesh_ip" ] && [ -n "$svc_port" ] && [ "$svc_port" != "0" ]; then
|
||||
[ $first_ep -eq 0 ] && endpoints="$endpoints,"
|
||||
first_ep=0
|
||||
endpoints="$endpoints{\"type\":\"mesh\",\"url\":\"http://${mesh_ip}:${svc_port}\",\"ssl\":false,\"primary\":true}"
|
||||
fi
|
||||
|
||||
# Local endpoint
|
||||
if [ -n "$svc_port" ] && [ "$svc_port" != "0" ]; then
|
||||
[ $first_ep -eq 0 ] && endpoints="$endpoints,"
|
||||
first_ep=0
|
||||
endpoints="$endpoints{\"type\":\"local\",\"url\":\"http://${LOCAL_IP}:${svc_port}\",\"ssl\":false,\"primary\":$([ $first_ep -eq 1 ] && echo true || echo false)}"
|
||||
fi
|
||||
|
||||
endpoints="$endpoints]"
|
||||
|
||||
cat << SVCEOF
|
||||
{
|
||||
"name": "$svc_name",
|
||||
"backend": "",
|
||||
"status": "$svc_status",
|
||||
"enabled": ${svc_enabled:-false},
|
||||
"port": "${svc_port:-}",
|
||||
"endpoints": $endpoints,
|
||||
"health": {
|
||||
"last_check": "$UPDATED",
|
||||
"status": "$([ \"$svc_status\" = \"running\" ] && echo healthy || echo unhealthy)",
|
||||
"latency_ms": 0
|
||||
}
|
||||
}
|
||||
SVCEOF
|
||||
fi
|
||||
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
|
||||
echo ']'
|
||||
}
|
||||
|
||||
# Get DNS hostname for this node
|
||||
LOCAL_DNS_HOST=$(get_dns_hostname "$LOCAL_NODE_NAME")
|
||||
|
||||
# Get linked peers for navigation
|
||||
get_peer_links() {
|
||||
local peers_file="/tmp/secubox-p2p-peers.json"
|
||||
[ -f "$peers_file" ] || { echo '[]'; return; }
|
||||
|
||||
local result='['
|
||||
local first=1
|
||||
local peer_count=$(jsonfilter -i "$peers_file" -e '@.peers[*]' 2>/dev/null | wc -l)
|
||||
local i=0
|
||||
|
||||
while [ $i -lt $peer_count ]; do
|
||||
local is_local=$(jsonfilter -i "$peers_file" -e "@.peers[$i].is_local" 2>/dev/null)
|
||||
[ "$is_local" = "true" ] && { i=$((i + 1)); continue; }
|
||||
|
||||
local peer_addr=$(jsonfilter -i "$peers_file" -e "@.peers[$i].address" 2>/dev/null)
|
||||
local peer_name=$(jsonfilter -i "$peers_file" -e "@.peers[$i].name" 2>/dev/null)
|
||||
local peer_status=$(jsonfilter -i "$peers_file" -e "@.peers[$i].status" 2>/dev/null)
|
||||
local peer_wg=$(jsonfilter -i "$peers_file" -e "@.peers[$i].wg_addresses" 2>/dev/null | cut -d',' -f1)
|
||||
|
||||
[ -n "$peer_addr" ] || { i=$((i + 1)); continue; }
|
||||
|
||||
local peer_dns=$(get_dns_hostname "$peer_name")
|
||||
|
||||
[ $first -eq 0 ] && result="$result,"
|
||||
first=0
|
||||
|
||||
result="$result{\"name\":\"$peer_name\",\"address\":\"$peer_addr\",\"status\":\"$peer_status\""
|
||||
result="$result,\"dns_hostname\":\"$peer_dns\""
|
||||
result="$result,\"dns_fqdn\":\"$peer_dns.$DNS_DOMAIN\""
|
||||
result="$result,\"wg_address\":\"$peer_wg\""
|
||||
result="$result,\"factory_url\":\"http://$peer_addr:7331/factory/\""
|
||||
result="$result,\"catalog_url\":\"http://$peer_addr:7331/api/factory/catalog\"}"
|
||||
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
result="$result]"
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# Main output
|
||||
cat << EOF
|
||||
{
|
||||
"node_id": "$LOCAL_NODE_ID",
|
||||
"node_name": "$LOCAL_NODE_NAME",
|
||||
"updated": "$UPDATED",
|
||||
"local_ip": "$LOCAL_IP",
|
||||
"wan_ip": "$(get_wan_ip)",
|
||||
"mesh_ip": "$(get_mesh_ip)",
|
||||
"dns": {
|
||||
"enabled": $DNS_ENABLED,
|
||||
"domain": "$DNS_DOMAIN",
|
||||
"hostname": "$LOCAL_DNS_HOST",
|
||||
"fqdn": "$LOCAL_DNS_HOST.$DNS_DOMAIN"
|
||||
},
|
||||
"linked_peers": $(get_peer_links),
|
||||
$(build_catalog)
|
||||
}
|
||||
EOF
|
||||
@ -137,6 +137,61 @@ case "$tool_id" in
|
||||
output="Merkle root: $merkle"
|
||||
;;
|
||||
|
||||
catalog-sync)
|
||||
result=$(catalog_sync 2>&1)
|
||||
output="Catalog sync result: $result"
|
||||
;;
|
||||
|
||||
catalog-list)
|
||||
result=$(catalog_list 2>&1)
|
||||
output="$result"
|
||||
;;
|
||||
|
||||
catalog-generate)
|
||||
result=$(catalog_generate_local 2>&1)
|
||||
output="Catalog generated: $result"
|
||||
;;
|
||||
|
||||
dns-status)
|
||||
if [ -x /usr/sbin/secubox-p2p ]; then
|
||||
result=$(/usr/sbin/secubox-p2p dns-status 2>&1)
|
||||
output="$result"
|
||||
else
|
||||
output='{"error":"p2p_unavailable"}'
|
||||
success=0
|
||||
fi
|
||||
;;
|
||||
|
||||
dns-enable)
|
||||
if [ -x /usr/sbin/secubox-p2p ]; then
|
||||
result=$(/usr/sbin/secubox-p2p dns-enable "mesh.local" 2>&1)
|
||||
output="DNS Federation enabled: $result"
|
||||
else
|
||||
output="P2P daemon not available"
|
||||
success=0
|
||||
fi
|
||||
;;
|
||||
|
||||
dns-disable)
|
||||
if [ -x /usr/sbin/secubox-p2p ]; then
|
||||
result=$(/usr/sbin/secubox-p2p dns-disable 2>&1)
|
||||
output="DNS Federation disabled: $result"
|
||||
else
|
||||
output="P2P daemon not available"
|
||||
success=0
|
||||
fi
|
||||
;;
|
||||
|
||||
dns-update)
|
||||
if [ -x /usr/sbin/secubox-p2p ]; then
|
||||
result=$(/usr/sbin/secubox-p2p dns-update 2>&1)
|
||||
output="DNS entries updated: $result"
|
||||
else
|
||||
output="P2P daemon not available"
|
||||
success=0
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
output="Unknown tool: $tool_id"
|
||||
success=0
|
||||
|
||||
@ -112,15 +112,73 @@ cat << 'EOF'
|
||||
"category": "security",
|
||||
"icon": "hash",
|
||||
"dangerous": false
|
||||
},
|
||||
{
|
||||
"id": "catalog-sync",
|
||||
"name": "Sync Catalog",
|
||||
"description": "Sync service catalog with mesh peers and merge registries",
|
||||
"category": "catalog",
|
||||
"icon": "book",
|
||||
"dangerous": false
|
||||
},
|
||||
{
|
||||
"id": "catalog-list",
|
||||
"name": "List Catalogs",
|
||||
"description": "Show local and peer catalog files",
|
||||
"category": "catalog",
|
||||
"icon": "list",
|
||||
"dangerous": false
|
||||
},
|
||||
{
|
||||
"id": "catalog-generate",
|
||||
"name": "Generate Catalog",
|
||||
"description": "Regenerate local service catalog from HAProxy vhosts",
|
||||
"category": "catalog",
|
||||
"icon": "refresh",
|
||||
"dangerous": false
|
||||
},
|
||||
{
|
||||
"id": "dns-status",
|
||||
"name": "DNS Federation Status",
|
||||
"description": "Show mesh DNS federation status and entries",
|
||||
"category": "dns",
|
||||
"icon": "globe",
|
||||
"dangerous": false
|
||||
},
|
||||
{
|
||||
"id": "dns-enable",
|
||||
"name": "Enable DNS Federation",
|
||||
"description": "Enable automatic DNS entries for mesh peers (.mesh.local)",
|
||||
"category": "dns",
|
||||
"icon": "toggle-on",
|
||||
"dangerous": false
|
||||
},
|
||||
{
|
||||
"id": "dns-disable",
|
||||
"name": "Disable DNS Federation",
|
||||
"description": "Disable mesh DNS federation",
|
||||
"category": "dns",
|
||||
"icon": "toggle-off",
|
||||
"dangerous": false
|
||||
},
|
||||
{
|
||||
"id": "dns-update",
|
||||
"name": "Update DNS Entries",
|
||||
"description": "Refresh DNS entries from current peer list",
|
||||
"category": "dns",
|
||||
"icon": "refresh",
|
||||
"dangerous": false
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{"id": "security", "name": "Security", "order": 1},
|
||||
{"id": "mesh", "name": "Mesh Network", "order": 2},
|
||||
{"id": "monitoring", "name": "Monitoring", "order": 3},
|
||||
{"id": "maintenance", "name": "Maintenance", "order": 4},
|
||||
{"id": "backup", "name": "Backup", "order": 5},
|
||||
{"id": "queue", "name": "Queue", "order": 6}
|
||||
{"id": "dns", "name": "DNS Federation", "order": 3},
|
||||
{"id": "catalog", "name": "Catalog", "order": 4},
|
||||
{"id": "monitoring", "name": "Monitoring", "order": 5},
|
||||
{"id": "maintenance", "name": "Maintenance", "order": 6},
|
||||
{"id": "backup", "name": "Backup", "order": 7},
|
||||
{"id": "queue", "name": "Queue", "order": 8}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
@ -98,12 +98,12 @@
|
||||
/* 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); }
|
||||
/* Emoji nav */
|
||||
.emoji-nav { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.emoji-btn { font-size: 1.5rem; padding: 0.5rem; background: var(--card); border: 2px solid transparent; border-radius: 0.5rem; cursor: pointer; transition: all 0.2s; filter: grayscale(0.5); opacity: 0.7; }
|
||||
.emoji-btn:hover { filter: grayscale(0); opacity: 1; transform: scale(1.1); }
|
||||
.emoji-btn.active { filter: grayscale(0); opacity: 1; border-color: var(--accent); background: rgba(99, 102, 241, 0.2); }
|
||||
.emoji-btn[title]:hover::after { content: attr(title); position: absolute; bottom: -1.5rem; left: 50%; transform: translateX(-50%); font-size: 0.65rem; background: var(--card); padding: 0.2rem 0.4rem; border-radius: 0.2rem; white-space: nowrap; }
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
@ -137,6 +137,78 @@
|
||||
.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; }
|
||||
|
||||
/* Health emoji status */
|
||||
.health-emoji { cursor: pointer; font-size: 1rem; transition: transform 0.2s; }
|
||||
.health-emoji:hover { transform: scale(1.3); }
|
||||
.health-emoji.clickable { cursor: pointer; }
|
||||
|
||||
/* Service URL display */
|
||||
.service-url { font-size: 0.65rem; color: var(--accent); font-family: ui-monospace, monospace; word-break: break-all; }
|
||||
.service-url a { color: var(--accent); text-decoration: none; }
|
||||
.service-url a:hover { text-decoration: underline; }
|
||||
|
||||
/* QR code modal */
|
||||
.qr-modal { text-align: center; }
|
||||
.qr-modal canvas { margin: 1rem auto; display: block; background: white; padding: 1rem; border-radius: 0.5rem; }
|
||||
.qr-url { font-family: ui-monospace, monospace; font-size: 0.8rem; color: var(--accent); margin-top: 0.5rem; word-break: break-all; }
|
||||
|
||||
/* Stat emoji clickable */
|
||||
.stat-emoji { font-size: 1.5rem; cursor: pointer; transition: transform 0.2s; }
|
||||
.stat-emoji:hover { transform: scale(1.2); }
|
||||
|
||||
/* Catalog styles */
|
||||
.catalog-service { background: var(--card); border-radius: 0.5rem; overflow: hidden; margin-bottom: 0.75rem; }
|
||||
.catalog-service-header { padding: 0.75rem 1rem; background: rgba(0,0,0,0.2); display: flex; justify-content: space-between; align-items: center; cursor: pointer; border-bottom: 1px solid var(--border); }
|
||||
.catalog-service-header:hover { background: rgba(0,0,0,0.3); }
|
||||
.catalog-service-name { font-size: 1rem; font-weight: 600; display: flex; align-items: center; gap: 0.5rem; }
|
||||
.catalog-service-meta { display: flex; gap: 0.5rem; align-items: center; }
|
||||
.catalog-endpoints { padding: 0; max-height: 0; overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; }
|
||||
.catalog-endpoints.open { padding: 0.75rem 1rem; max-height: 500px; }
|
||||
.endpoint-item { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0.75rem; margin-bottom: 0.5rem; background: rgba(0,0,0,0.15); border-radius: 0.25rem; border-left: 3px solid var(--border); }
|
||||
.endpoint-item.primary { border-left-color: var(--accent); }
|
||||
.endpoint-item.haproxy { border-left-color: var(--success); }
|
||||
.endpoint-item.mesh { border-left-color: #818cf8; }
|
||||
.endpoint-item.local { border-left-color: var(--muted); }
|
||||
.endpoint-type { font-size: 0.65rem; text-transform: uppercase; padding: 0.15rem 0.4rem; border-radius: 0.15rem; background: var(--border); margin-right: 0.5rem; }
|
||||
.endpoint-type.haproxy { background: var(--success); color: #000; }
|
||||
.endpoint-type.mesh { background: #818cf8; }
|
||||
.endpoint-type.local { background: var(--muted); }
|
||||
.endpoint-url { flex: 1; font-family: ui-monospace, monospace; font-size: 0.8rem; color: var(--accent); word-break: break-all; }
|
||||
.endpoint-url a { color: inherit; text-decoration: none; }
|
||||
.endpoint-url a:hover { text-decoration: underline; }
|
||||
.endpoint-actions { display: flex; gap: 0.35rem; }
|
||||
.endpoint-badge { font-size: 0.6rem; padding: 0.1rem 0.3rem; border-radius: 0.15rem; background: var(--accent); }
|
||||
.endpoint-badge.ssl { background: var(--success); color: #000; }
|
||||
|
||||
/* Catalog node grouping */
|
||||
.catalog-node-group { margin-bottom: 1.5rem; }
|
||||
.catalog-node-header { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; border-bottom: 2px solid var(--accent); margin-bottom: 0.75rem; }
|
||||
.catalog-node-name { font-size: 1.1rem; font-weight: 700; display: flex; align-items: center; gap: 0.5rem; }
|
||||
.catalog-node-stats { font-size: 0.75rem; color: var(--muted); }
|
||||
|
||||
/* URL tabs in QR modal */
|
||||
.url-tabs { display: flex; gap: 0.25rem; margin-bottom: 0.75rem; flex-wrap: wrap; justify-content: center; }
|
||||
.url-tab { padding: 0.35rem 0.6rem; border-radius: 0.25rem; font-size: 0.7rem; cursor: pointer; background: var(--border); border: none; color: var(--text); }
|
||||
.url-tab.active { background: var(--accent); }
|
||||
.url-tab.haproxy { border-bottom: 2px solid var(--success); }
|
||||
.url-tab.mesh { border-bottom: 2px solid #818cf8; }
|
||||
.url-tab.local { border-bottom: 2px solid var(--muted); }
|
||||
|
||||
/* Linked peers / mesh navigation */
|
||||
.peer-links { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; padding: 0.75rem; background: var(--card); border-radius: 0.5rem; border-left: 3px solid #818cf8; }
|
||||
.peer-links-title { font-size: 0.75rem; color: var(--muted); width: 100%; margin-bottom: 0.25rem; }
|
||||
.peer-link { display: flex; align-items: center; gap: 0.35rem; padding: 0.35rem 0.6rem; background: rgba(0,0,0,0.2); border-radius: 0.25rem; font-size: 0.75rem; cursor: pointer; transition: all 0.2s; text-decoration: none; color: var(--text); }
|
||||
.peer-link:hover { background: var(--accent); transform: translateY(-1px); }
|
||||
.peer-link.online { border-left: 2px solid var(--success); }
|
||||
.peer-link.offline { border-left: 2px solid var(--danger); opacity: 0.6; }
|
||||
.peer-link-name { font-weight: 500; }
|
||||
.peer-link-dns { font-family: ui-monospace, monospace; font-size: 0.65rem; color: var(--accent); }
|
||||
|
||||
/* DNS info badge */
|
||||
.dns-info { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.25rem 0.5rem; background: rgba(129, 140, 248, 0.2); border-radius: 0.25rem; font-size: 0.7rem; font-family: ui-monospace, monospace; color: #818cf8; }
|
||||
.dns-enabled { color: var(--success); }
|
||||
.dns-disabled { color: var(--muted); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -145,19 +217,20 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21h18M9 8h6M9 12h6M9 16h6M5 21V5a2 2 0 012-2h10a2 2 0 012 2v16"/></svg>
|
||||
SecuBox Factory
|
||||
</div>
|
||||
<div class="emoji-nav">
|
||||
<button class="emoji-btn active" onclick="switchTab('dashboard')" title="Dashboard">🏭</button>
|
||||
<button class="emoji-btn" onclick="switchTab('services')" title="Mesh Services">🔗</button>
|
||||
<button class="emoji-btn" onclick="switchTab('catalog')" title="Distributed Catalog">📚</button>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="badge badge-muted" id="node-count">- nodes</span>
|
||||
<span class="badge" id="snapshot-status">...</span>
|
||||
<span class="fingerprint" id="fingerprint" title="Node Fingerprint">...</span>
|
||||
<button onclick="toggleTools()" title="Tools">Tools</button>
|
||||
<button onclick="toggleTools()" title="Tools">🧰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main style="padding: 1rem;">
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('dashboard')">Dashboard</button>
|
||||
<button class="tab" onclick="switchTab('services')">Mesh Services</button>
|
||||
</div>
|
||||
|
||||
<div id="dashboard" class="tab-content active" style="display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));">
|
||||
<div class="card accent">
|
||||
@ -170,6 +243,12 @@
|
||||
<div class="stat"><div class="spinner"></div><div class="stat-label">Loading services...</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="catalog" class="tab-content">
|
||||
<div class="card accent">
|
||||
<div class="stat"><div class="spinner"></div><div class="stat-label">Loading catalog...</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="overlay" id="overlay" onclick="toggleTools()"></div>
|
||||
@ -187,14 +266,27 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="qr-modal" class="qr-modal">
|
||||
<h3><span id="qr-title">Service Access</span><button onclick="closeQR()" style="background:transparent;font-size:1.25rem;padding:0.25rem;">×</button></h3>
|
||||
<canvas id="qr-canvas" width="200" height="200"></canvas>
|
||||
<div class="qr-url" id="qr-url"></div>
|
||||
<div class="actions">
|
||||
<button onclick="copyUrl()">📋 Copy URL</button>
|
||||
<button onclick="closeQR()">Close</button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
// State
|
||||
let data = { local: {}, peers: [], snapshot: {}, services: {}, system: {} };
|
||||
let meshServices = { nodes: [], summary: {} };
|
||||
let catalogData = { services: [], node_name: '', updated: '' };
|
||||
let tools = [];
|
||||
let refreshInterval = null;
|
||||
let activeTab = 'dashboard';
|
||||
let serviceFilter = { search: '', status: 'all', node: 'all' };
|
||||
let catalogFilter = { search: '', type: 'all', status: 'all' };
|
||||
let expandedServices = new Set();
|
||||
|
||||
// API helpers - use same origin (HAProxy routes /factory/* to API)
|
||||
const apiBase = '/factory/';
|
||||
@ -234,19 +326,47 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh catalog data
|
||||
async function refreshCatalog() {
|
||||
try {
|
||||
catalogData = await api.get('catalog');
|
||||
renderCatalog();
|
||||
} catch (e) {
|
||||
console.error('Catalog refresh failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Switch tabs
|
||||
function switchTab(tab) {
|
||||
activeTab = tab;
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.emoji-btn').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
document.querySelector(`.tab[onclick*="${tab}"]`).classList.add('active');
|
||||
document.querySelector(`.emoji-btn[onclick*="${tab}"]`).classList.add('active');
|
||||
document.getElementById(tab).classList.add('active');
|
||||
|
||||
if (tab === 'services') {
|
||||
refreshMeshServices();
|
||||
} else if (tab === 'catalog') {
|
||||
refreshCatalog();
|
||||
}
|
||||
}
|
||||
|
||||
// Filter catalog
|
||||
function filterCatalog(type, value) {
|
||||
catalogFilter[type] = value;
|
||||
renderCatalog();
|
||||
}
|
||||
|
||||
// Toggle service expansion
|
||||
function toggleServiceExpand(name) {
|
||||
if (expandedServices.has(name)) {
|
||||
expandedServices.delete(name);
|
||||
} else {
|
||||
expandedServices.add(name);
|
||||
}
|
||||
renderCatalog();
|
||||
}
|
||||
|
||||
// Filter services
|
||||
function filterServices(type, value) {
|
||||
serviceFilter[type] = value;
|
||||
@ -472,19 +592,20 @@
|
||||
html += `<div style="padding: 1rem; text-align: center; color: var(--muted);">No matching services</div>`;
|
||||
} else {
|
||||
services.forEach(svc => {
|
||||
const statusClass = svc.status === 'running' ? 'running' : (svc.enabled ? 'stopped' : 'disabled');
|
||||
const hasPort = svc.port && svc.port !== '' && svc.port !== '0';
|
||||
const accessUrl = hasPort ? (node.is_local ? `http://${location.hostname}:${svc.port}` : `http://${nodeAddr}:${svc.port}`) : null;
|
||||
const healthEmoji = svc.status === 'running' ? '🟢' : (svc.enabled ? '🔴' : '⚫');
|
||||
const healthTitle = svc.status === 'running' ? 'Running' : (svc.enabled ? 'Stopped' : 'Disabled');
|
||||
|
||||
html += `
|
||||
<div class="service-item">
|
||||
<div class="service-info">
|
||||
<div class="service-name">${svc.name}</div>
|
||||
${hasPort ? `<div class="service-port">Port: ${svc.port}</div>` : ''}
|
||||
${accessUrl ? `<div class="service-url"><a href="${accessUrl}" target="_blank">${accessUrl}</a></div>` : (hasPort ? `<div class="service-port">:${svc.port}</div>` : '')}
|
||||
</div>
|
||||
<div class="service-status">
|
||||
${accessUrl ? `<a href="${accessUrl}" target="_blank" class="service-link">Open</a>` : ''}
|
||||
<span class="status-dot ${statusClass}" title="${svc.status}"></span>
|
||||
${accessUrl ? `<span class="health-emoji clickable" onclick="showQR('${svc.name}', '${accessUrl}')" title="Show QR">📱</span>` : ''}
|
||||
<span class="health-emoji" title="${healthTitle}" onclick="toggleService('${svc.name}', '${svc.status}')">${healthEmoji}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
@ -500,6 +621,262 @@
|
||||
document.getElementById('services').innerHTML = html;
|
||||
}
|
||||
|
||||
// Render catalog panel
|
||||
function renderCatalog() {
|
||||
const services = catalogData.services || [];
|
||||
const nodeName = catalogData.node_name || 'Local';
|
||||
const nodeId = catalogData.node_id || '';
|
||||
const updated = catalogData.updated || '';
|
||||
const dns = catalogData.dns || { enabled: false, domain: 'mesh.local', hostname: '', fqdn: '' };
|
||||
const linkedPeers = catalogData.linked_peers || [];
|
||||
|
||||
// Count stats
|
||||
let totalServices = services.length;
|
||||
let runningServices = services.filter(s => s.status === 'running').length;
|
||||
let haproxyCount = 0;
|
||||
let meshCount = 0;
|
||||
let localCount = 0;
|
||||
|
||||
services.forEach(s => {
|
||||
(s.endpoints || []).forEach(ep => {
|
||||
if (ep.type === 'haproxy') haproxyCount++;
|
||||
else if (ep.type === 'mesh') meshCount++;
|
||||
else if (ep.type === 'local') localCount++;
|
||||
});
|
||||
});
|
||||
|
||||
// Filter services
|
||||
let filteredServices = services;
|
||||
if (catalogFilter.search) {
|
||||
const search = catalogFilter.search.toLowerCase();
|
||||
filteredServices = filteredServices.filter(s => s.name.toLowerCase().includes(search));
|
||||
}
|
||||
if (catalogFilter.status !== 'all') {
|
||||
filteredServices = filteredServices.filter(s => s.status === catalogFilter.status);
|
||||
}
|
||||
if (catalogFilter.type !== 'all') {
|
||||
filteredServices = filteredServices.filter(s =>
|
||||
(s.endpoints || []).some(ep => ep.type === catalogFilter.type)
|
||||
);
|
||||
}
|
||||
|
||||
// Build linked peers section
|
||||
let peersHtml = '';
|
||||
if (linkedPeers.length > 0) {
|
||||
peersHtml = `
|
||||
<div class="peer-links">
|
||||
<div class="peer-links-title">🔗 Linked Mesh Nodes</div>
|
||||
${linkedPeers.map(peer => `
|
||||
<a href="${peer.factory_url}" target="_blank" class="peer-link ${peer.status}" title="Open ${peer.name} Factory Dashboard">
|
||||
<span>${peer.status === 'online' ? '🟢' : '🔴'}</span>
|
||||
<span class="peer-link-name">${peer.name}</span>
|
||||
${dns.enabled && peer.dns_fqdn ? `<span class="peer-link-dns">${peer.dns_fqdn}</span>` : ''}
|
||||
</a>
|
||||
`).join('')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let html = `
|
||||
<div class="services-summary">
|
||||
<div class="summary-item">
|
||||
<span class="summary-count">${runningServices}/${totalServices}</span>
|
||||
<span class="summary-label">Services</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-count" style="color: var(--success);">${haproxyCount}</span>
|
||||
<span class="summary-label">HAProxy URLs</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-count" style="color: #818cf8;">${meshCount}</span>
|
||||
<span class="summary-label">Mesh URLs</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-count" style="color: var(--muted);">${localCount}</span>
|
||||
<span class="summary-label">Local URLs</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="dns-info ${dns.enabled ? 'dns-enabled' : 'dns-disabled'}">
|
||||
${dns.enabled ? '🌐' : '⚫'} ${dns.enabled ? dns.fqdn : 'DNS off'}
|
||||
</span>
|
||||
</div>
|
||||
<button class="sm" onclick="refreshCatalog()">Refresh</button>
|
||||
<button class="sm primary" onclick="syncCatalog()">Sync Peers</button>
|
||||
</div>
|
||||
|
||||
${peersHtml}
|
||||
|
||||
<div class="filter-bar">
|
||||
<input type="text" class="filter-input" placeholder="Search services..."
|
||||
value="${catalogFilter.search}"
|
||||
oninput="filterCatalog('search', this.value)">
|
||||
<select class="filter-select" onchange="filterCatalog('status', this.value)">
|
||||
<option value="all" ${catalogFilter.status === 'all' ? 'selected' : ''}>All Status</option>
|
||||
<option value="running" ${catalogFilter.status === 'running' ? 'selected' : ''}>Running</option>
|
||||
<option value="stopped" ${catalogFilter.status === 'stopped' ? 'selected' : ''}>Stopped</option>
|
||||
</select>
|
||||
<select class="filter-select" onchange="filterCatalog('type', this.value)">
|
||||
<option value="all" ${catalogFilter.type === 'all' ? 'selected' : ''}>All Endpoints</option>
|
||||
<option value="haproxy" ${catalogFilter.type === 'haproxy' ? 'selected' : ''}>HAProxy (WAN)</option>
|
||||
<option value="mesh" ${catalogFilter.type === 'mesh' ? 'selected' : ''}>Mesh (WG)</option>
|
||||
<option value="local" ${catalogFilter.type === 'local' ? 'selected' : ''}>Local (LAN)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="catalog-node-group">
|
||||
<div class="catalog-node-header">
|
||||
<div class="catalog-node-name">
|
||||
<span>📍</span> ${nodeName}
|
||||
</div>
|
||||
<div class="catalog-node-stats">
|
||||
${updated ? `Updated: ${new Date(updated).toLocaleString()}` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (filteredServices.length === 0) {
|
||||
html += `<div class="card"><div style="text-align: center; padding: 2rem; color: var(--muted);">No services found matching filters</div></div>`;
|
||||
} else {
|
||||
filteredServices.forEach(svc => {
|
||||
const isExpanded = expandedServices.has(svc.name);
|
||||
const healthEmoji = svc.status === 'running' ? '🟢' : (svc.enabled ? '🔴' : '⚫');
|
||||
const endpoints = svc.endpoints || [];
|
||||
const primaryEndpoint = endpoints.find(ep => ep.primary) || endpoints[0];
|
||||
const endpointCount = endpoints.length;
|
||||
|
||||
html += `
|
||||
<div class="catalog-service">
|
||||
<div class="catalog-service-header" onclick="toggleServiceExpand('${svc.name}')">
|
||||
<div class="catalog-service-name">
|
||||
<span>${healthEmoji}</span>
|
||||
<span>${svc.name}</span>
|
||||
${svc.port ? `<span class="badge badge-muted">:${svc.port}</span>` : ''}
|
||||
</div>
|
||||
<div class="catalog-service-meta">
|
||||
<span class="badge badge-muted">${endpointCount} URL${endpointCount !== 1 ? 's' : ''}</span>
|
||||
${primaryEndpoint ? `<span class="badge ${primaryEndpoint.ssl ? 'badge-ok' : 'badge-muted'}">${primaryEndpoint.type}</span>` : ''}
|
||||
<span style="font-size: 0.8rem;">${isExpanded ? '▼' : '▶'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="catalog-endpoints ${isExpanded ? 'open' : ''}">`;
|
||||
|
||||
// Sort endpoints: haproxy first, then mesh, then local
|
||||
const sortOrder = { haproxy: 0, mesh: 1, local: 2 };
|
||||
const sortedEndpoints = [...endpoints].sort((a, b) =>
|
||||
(sortOrder[a.type] || 99) - (sortOrder[b.type] || 99)
|
||||
);
|
||||
|
||||
sortedEndpoints.forEach(ep => {
|
||||
const isPrimary = ep.primary === true;
|
||||
html += `
|
||||
<div class="endpoint-item ${ep.type} ${isPrimary ? 'primary' : ''}">
|
||||
<span class="endpoint-type ${ep.type}">${ep.type}</span>
|
||||
<div class="endpoint-url">
|
||||
<a href="${ep.url}" target="_blank">${ep.url}</a>
|
||||
</div>
|
||||
<div class="endpoint-actions">
|
||||
${ep.ssl ? '<span class="endpoint-badge ssl">SSL</span>' : ''}
|
||||
${isPrimary ? '<span class="endpoint-badge">Primary</span>' : ''}
|
||||
<button class="sm" onclick="event.stopPropagation(); showQRMulti('${svc.name}', ${JSON.stringify(endpoints).replace(/"/g, '"')}, '${ep.url}')" title="QR Code">📱</button>
|
||||
<button class="sm" onclick="event.stopPropagation(); copyToClipboard('${ep.url}')" title="Copy URL">📋</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
document.getElementById('catalog').innerHTML = html;
|
||||
}
|
||||
|
||||
// Sync catalog with peers
|
||||
async function syncCatalog() {
|
||||
const modal = document.getElementById('modal');
|
||||
document.getElementById('modal-title').textContent = 'Catalog Sync';
|
||||
document.getElementById('modal-output').innerHTML = '<span class="spinner"></span> Syncing catalogs with peers...';
|
||||
modal.showModal();
|
||||
|
||||
try {
|
||||
const result = await api.post('run', { tool: 'catalog-sync' });
|
||||
if (result.success) {
|
||||
document.getElementById('modal-output').textContent = result.output || 'Sync completed';
|
||||
refreshCatalog();
|
||||
} else {
|
||||
document.getElementById('modal-output').textContent = 'Error: ' + (result.error || 'Sync failed');
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('modal-output').textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Show QR with multiple URL options
|
||||
function showQRMulti(name, endpoints, selectedUrl) {
|
||||
currentQRUrl = selectedUrl;
|
||||
document.getElementById('qr-title').textContent = name;
|
||||
|
||||
// Build URL tabs
|
||||
let tabsHtml = '<div class="url-tabs">';
|
||||
endpoints.forEach(ep => {
|
||||
const isActive = ep.url === selectedUrl;
|
||||
tabsHtml += `<button class="url-tab ${ep.type} ${isActive ? 'active' : ''}" onclick="switchQRUrl('${name}', '${ep.url}', '${ep.type}')">${ep.type}${ep.ssl ? ' 🔒' : ''}</button>`;
|
||||
});
|
||||
tabsHtml += '</div>';
|
||||
|
||||
// Insert tabs before URL display
|
||||
const qrUrlEl = document.getElementById('qr-url');
|
||||
qrUrlEl.innerHTML = tabsHtml + `<div style="margin-top: 0.5rem;">${selectedUrl}</div>`;
|
||||
|
||||
// Generate QR
|
||||
const canvas = document.getElementById('qr-canvas');
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => {
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, 200, 200);
|
||||
ctx.drawImage(img, 0, 0, 200, 200);
|
||||
};
|
||||
img.src = `https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=${encodeURIComponent(selectedUrl)}&choe=UTF-8`;
|
||||
|
||||
document.getElementById('qr-modal').showModal();
|
||||
}
|
||||
|
||||
// Switch QR URL tab
|
||||
function switchQRUrl(name, url, type) {
|
||||
currentQRUrl = url;
|
||||
|
||||
// Update active tab
|
||||
document.querySelectorAll('.url-tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelector(`.url-tab.${type}`).classList.add('active');
|
||||
|
||||
// Update URL display
|
||||
const qrUrlEl = document.getElementById('qr-url');
|
||||
const tabsHtml = qrUrlEl.querySelector('.url-tabs').outerHTML;
|
||||
qrUrlEl.innerHTML = tabsHtml + `<div style="margin-top: 0.5rem;">${url}</div>`;
|
||||
|
||||
// Regenerate QR
|
||||
const canvas = document.getElementById('qr-canvas');
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => {
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, 200, 200);
|
||||
ctx.drawImage(img, 0, 0, 200, 200);
|
||||
};
|
||||
img.src = `https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=${encodeURIComponent(url)}&choe=UTF-8`;
|
||||
}
|
||||
|
||||
// Copy to clipboard helper
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
// Brief visual feedback could be added here
|
||||
});
|
||||
}
|
||||
|
||||
// Render tools panel
|
||||
function renderTools(categories) {
|
||||
const grouped = {};
|
||||
@ -559,10 +936,51 @@
|
||||
document.getElementById('modal').close();
|
||||
}
|
||||
|
||||
// QR Code functions
|
||||
let currentQRUrl = '';
|
||||
|
||||
function showQR(name, url) {
|
||||
currentQRUrl = url;
|
||||
document.getElementById('qr-title').textContent = name;
|
||||
document.getElementById('qr-url').textContent = url;
|
||||
|
||||
// Simple QR code using Google Charts API (zero deps)
|
||||
const canvas = document.getElementById('qr-canvas');
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => {
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, 200, 200);
|
||||
ctx.drawImage(img, 0, 0, 200, 200);
|
||||
};
|
||||
img.src = `https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=${encodeURIComponent(url)}&choe=UTF-8`;
|
||||
|
||||
document.getElementById('qr-modal').showModal();
|
||||
}
|
||||
|
||||
function closeQR() {
|
||||
document.getElementById('qr-modal').close();
|
||||
}
|
||||
|
||||
function copyUrl() {
|
||||
navigator.clipboard.writeText(currentQRUrl).then(() => {
|
||||
const btn = event.target;
|
||||
btn.textContent = '✓ Copied!';
|
||||
setTimeout(() => btn.textContent = '📋 Copy URL', 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleService(name, status) {
|
||||
// Could be extended to start/stop services
|
||||
console.log(`Toggle service ${name} (currently ${status})`);
|
||||
}
|
||||
|
||||
// Handle ESC key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (document.getElementById('modal').open) closeModal();
|
||||
if (document.getElementById('qr-modal').open) closeQR();
|
||||
else if (document.getElementById('modal').open) closeModal();
|
||||
else if (document.getElementById('tools').classList.contains('open')) toggleTools();
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user