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:
CyberMind-FR 2026-01-31 09:19:36 +01:00
parent cf115b346a
commit eec83efa13
10 changed files with 1412 additions and 98 deletions

View File

@ -20,7 +20,7 @@ Architecture: all
Installed-Size: 378880 Installed-Size: 378880
Description: Advanced bandwidth management with QoS rules, client quotas, and SQM integration Description: Advanced bandwidth management with QoS rules, client quotas, and SQM integration
Filename: luci-app-bandwidth-manager_0.5.0-r2_all.ipk Filename: luci-app-bandwidth-manager_0.5.0-r2_all.ipk
Size: 66966 Size: 66972
Package: luci-app-cdn-cache Package: luci-app-cdn-cache
Version: 0.5.0-r3 Version: 0.5.0-r3
@ -32,7 +32,7 @@ Architecture: all
Installed-Size: 122880 Installed-Size: 122880
Description: Dashboard for managing local CDN caching proxy on OpenWrt Description: Dashboard for managing local CDN caching proxy on OpenWrt
Filename: luci-app-cdn-cache_0.5.0-r3_all.ipk Filename: luci-app-cdn-cache_0.5.0-r3_all.ipk
Size: 23189 Size: 23190
Package: luci-app-client-guardian Package: luci-app-client-guardian
Version: 0.4.0-r7 Version: 0.4.0-r7
@ -44,7 +44,7 @@ Architecture: all
Installed-Size: 307200 Installed-Size: 307200
Description: Network Access Control with client monitoring, zone management, captive portal, parental controls, and SMS/email alerts 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 Filename: luci-app-client-guardian_0.4.0-r7_all.ipk
Size: 57042 Size: 57045
Package: luci-app-crowdsec-dashboard Package: luci-app-crowdsec-dashboard
Version: 0.7.0-r29 Version: 0.7.0-r29
@ -56,7 +56,7 @@ Architecture: all
Installed-Size: 296960 Installed-Size: 296960
Description: Real-time security monitoring dashboard for CrowdSec on OpenWrt Description: Real-time security monitoring dashboard for CrowdSec on OpenWrt
Filename: luci-app-crowdsec-dashboard_0.7.0-r29_all.ipk Filename: luci-app-crowdsec-dashboard_0.7.0-r29_all.ipk
Size: 55585 Size: 55583
Package: luci-app-cyberfeed Package: luci-app-cyberfeed
Version: 0.1.1-r1 Version: 0.1.1-r1
@ -68,7 +68,7 @@ Architecture: all
Installed-Size: 71680 Installed-Size: 71680
Description: Cyberpunk-themed RSS feed aggregator dashboard with social media support Description: Cyberpunk-themed RSS feed aggregator dashboard with social media support
Filename: luci-app-cyberfeed_0.1.1-r1_all.ipk Filename: luci-app-cyberfeed_0.1.1-r1_all.ipk
Size: 12838 Size: 12841
Package: luci-app-exposure Package: luci-app-exposure
Version: 1.0.0-r3 Version: 1.0.0-r3
@ -92,7 +92,7 @@ Architecture: all
Installed-Size: 92160 Installed-Size: 92160
Description: Modern dashboard for Gitea Platform management on OpenWrt Description: Modern dashboard for Gitea Platform management on OpenWrt
Filename: luci-app-gitea_1.0.0-r2_all.ipk Filename: luci-app-gitea_1.0.0-r2_all.ipk
Size: 15585 Size: 15589
Package: luci-app-glances Package: luci-app-glances
Version: 1.0.0-r2 Version: 1.0.0-r2
@ -104,7 +104,7 @@ Architecture: all
Installed-Size: 40960 Installed-Size: 40960
Description: Modern dashboard for Glances system monitoring with SecuBox theme Description: Modern dashboard for Glances system monitoring with SecuBox theme
Filename: luci-app-glances_1.0.0-r2_all.ipk Filename: luci-app-glances_1.0.0-r2_all.ipk
Size: 6963 Size: 6968
Package: luci-app-haproxy Package: luci-app-haproxy
Version: 1.0.0-r8 Version: 1.0.0-r8
@ -116,7 +116,7 @@ Architecture: all
Installed-Size: 204800 Installed-Size: 204800
Description: Web interface for managing HAProxy load balancer with vhosts, SSL certificates, and backend routing 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 Filename: luci-app-haproxy_1.0.0-r8_all.ipk
Size: 34168 Size: 34166
Package: luci-app-hexojs Package: luci-app-hexojs
Version: 1.0.0-r3 Version: 1.0.0-r3
@ -128,7 +128,7 @@ Architecture: all
Installed-Size: 215040 Installed-Size: 215040
Description: Modern dashboard for Hexo static site generator on OpenWrt Description: Modern dashboard for Hexo static site generator on OpenWrt
Filename: luci-app-hexojs_1.0.0-r3_all.ipk Filename: luci-app-hexojs_1.0.0-r3_all.ipk
Size: 32974 Size: 32975
Package: luci-app-ksm-manager Package: luci-app-ksm-manager
Version: 0.4.0-r2 Version: 0.4.0-r2
@ -140,7 +140,7 @@ Architecture: all
Installed-Size: 112640 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. 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 Filename: luci-app-ksm-manager_0.4.0-r2_all.ipk
Size: 18720 Size: 18721
Package: luci-app-localai Package: luci-app-localai
Version: 0.1.0-r15 Version: 0.1.0-r15
@ -164,7 +164,7 @@ Architecture: all
Installed-Size: 40960 Installed-Size: 40960
Description: LuCI support for Lyrion Music Server Description: LuCI support for Lyrion Music Server
Filename: luci-app-lyrion_1.0.0-r1_all.ipk Filename: luci-app-lyrion_1.0.0-r1_all.ipk
Size: 6725 Size: 6730
Package: luci-app-magicmirror2 Package: luci-app-magicmirror2
Version: 0.4.0-r6 Version: 0.4.0-r6
@ -176,7 +176,7 @@ Architecture: all
Installed-Size: 71680 Installed-Size: 71680
Description: Modern dashboard for MagicMirror2 smart display platform with module manager and SecuBox theme Description: Modern dashboard for MagicMirror2 smart display platform with module manager and SecuBox theme
Filename: luci-app-magicmirror2_0.4.0-r6_all.ipk Filename: luci-app-magicmirror2_0.4.0-r6_all.ipk
Size: 12274 Size: 12277
Package: luci-app-mailinabox Package: luci-app-mailinabox
Version: 1.0.0-r1 Version: 1.0.0-r1
@ -188,7 +188,7 @@ Architecture: all
Installed-Size: 30720 Installed-Size: 30720
Description: LuCI support for Mail-in-a-Box Description: LuCI support for Mail-in-a-Box
Filename: luci-app-mailinabox_1.0.0-r1_all.ipk Filename: luci-app-mailinabox_1.0.0-r1_all.ipk
Size: 5481 Size: 5482
Package: luci-app-media-flow Package: luci-app-media-flow
Version: 0.6.4-r1 Version: 0.6.4-r1
@ -200,7 +200,7 @@ Architecture: all
Installed-Size: 102400 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. 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 Filename: luci-app-media-flow_0.6.4-r1_all.ipk
Size: 19120 Size: 19117
Package: luci-app-metablogizer Package: luci-app-metablogizer
Version: 1.0.0-r3 Version: 1.0.0-r3
@ -212,7 +212,7 @@ Architecture: all
Installed-Size: 112640 Installed-Size: 112640
Description: LuCI support for MetaBlogizer Static Site Publisher Description: LuCI support for MetaBlogizer Static Site Publisher
Filename: luci-app-metablogizer_1.0.0-r3_all.ipk Filename: luci-app-metablogizer_1.0.0-r3_all.ipk
Size: 23503 Size: 23501
Package: luci-app-metabolizer Package: luci-app-metabolizer
Version: 1.0.0-r2 Version: 1.0.0-r2
@ -236,7 +236,7 @@ Architecture: all
Installed-Size: 102400 Installed-Size: 102400
Description: Modern dashboard for mitmproxy HTTPS traffic inspection with SecuBox theme Description: Modern dashboard for mitmproxy HTTPS traffic inspection with SecuBox theme
Filename: luci-app-mitmproxy_0.4.0-r6_all.ipk Filename: luci-app-mitmproxy_0.4.0-r6_all.ipk
Size: 18933 Size: 18932
Package: luci-app-mmpm Package: luci-app-mmpm
Version: 0.2.0-r3 Version: 0.2.0-r3
@ -248,7 +248,7 @@ Architecture: all
Installed-Size: 51200 Installed-Size: 51200
Description: Web interface for MMPM - MagicMirror Package Manager Description: Web interface for MMPM - MagicMirror Package Manager
Filename: luci-app-mmpm_0.2.0-r3_all.ipk Filename: luci-app-mmpm_0.2.0-r3_all.ipk
Size: 7899 Size: 7901
Package: luci-app-mqtt-bridge Package: luci-app-mqtt-bridge
Version: 0.4.0-r4 Version: 0.4.0-r4
@ -260,7 +260,7 @@ Architecture: all
Installed-Size: 122880 Installed-Size: 122880
Description: USB-to-MQTT IoT hub with SecuBox theme Description: USB-to-MQTT IoT hub with SecuBox theme
Filename: luci-app-mqtt-bridge_0.4.0-r4_all.ipk Filename: luci-app-mqtt-bridge_0.4.0-r4_all.ipk
Size: 22777 Size: 22779
Package: luci-app-ndpid Package: luci-app-ndpid
Version: 1.1.2-r2 Version: 1.1.2-r2
@ -272,7 +272,7 @@ Architecture: all
Installed-Size: 122880 Installed-Size: 122880
Description: Modern dashboard for nDPId deep packet inspection on OpenWrt Description: Modern dashboard for nDPId deep packet inspection on OpenWrt
Filename: luci-app-ndpid_1.1.2-r2_all.ipk Filename: luci-app-ndpid_1.1.2-r2_all.ipk
Size: 22455 Size: 22457
Package: luci-app-netdata-dashboard Package: luci-app-netdata-dashboard
Version: 0.5.0-r2 Version: 0.5.0-r2
@ -284,7 +284,7 @@ Architecture: all
Installed-Size: 133120 Installed-Size: 133120
Description: Real-time system monitoring dashboard with Netdata integration for OpenWrt Description: Real-time system monitoring dashboard with Netdata integration for OpenWrt
Filename: luci-app-netdata-dashboard_0.5.0-r2_all.ipk Filename: luci-app-netdata-dashboard_0.5.0-r2_all.ipk
Size: 22396 Size: 22400
Package: luci-app-network-modes Package: luci-app-network-modes
Version: 0.5.0-r3 Version: 0.5.0-r3
@ -296,7 +296,7 @@ Architecture: all
Installed-Size: 307200 Installed-Size: 307200
Description: Configure OpenWrt for different network modes: Sniffer, Access Point, Relay, Router Description: Configure OpenWrt for different network modes: Sniffer, Access Point, Relay, Router
Filename: luci-app-network-modes_0.5.0-r3_all.ipk Filename: luci-app-network-modes_0.5.0-r3_all.ipk
Size: 55608 Size: 55613
Package: luci-app-network-tweaks Package: luci-app-network-tweaks
Version: 1.0.0-r7 Version: 1.0.0-r7
@ -308,7 +308,7 @@ Architecture: all
Installed-Size: 81920 Installed-Size: 81920
Description: Unified network services dashboard with DNS/hosts sync, CDN cache control, and WPAD auto-proxy configuration 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 Filename: luci-app-network-tweaks_1.0.0-r7_all.ipk
Size: 15455 Size: 15466
Package: luci-app-nextcloud Package: luci-app-nextcloud
Version: 1.0.0-r1 Version: 1.0.0-r1
@ -320,7 +320,7 @@ Architecture: all
Installed-Size: 30720 Installed-Size: 30720
Description: LuCI support for Nextcloud Description: LuCI support for Nextcloud
Filename: luci-app-nextcloud_1.0.0-r1_all.ipk Filename: luci-app-nextcloud_1.0.0-r1_all.ipk
Size: 6485 Size: 6487
Package: luci-app-ollama Package: luci-app-ollama
Version: 0.1.0-r1 Version: 0.1.0-r1
@ -332,7 +332,7 @@ Architecture: all
Installed-Size: 71680 Installed-Size: 71680
Description: Modern dashboard for Ollama LLM management on OpenWrt Description: Modern dashboard for Ollama LLM management on OpenWrt
Filename: luci-app-ollama_0.1.0-r1_all.ipk Filename: luci-app-ollama_0.1.0-r1_all.ipk
Size: 11991 Size: 11998
Package: luci-app-picobrew Package: luci-app-picobrew
Version: 1.0.0-r1 Version: 1.0.0-r1
@ -344,7 +344,7 @@ Architecture: all
Installed-Size: 51200 Installed-Size: 51200
Description: Modern dashboard for PicoBrew Server management on OpenWrt Description: Modern dashboard for PicoBrew Server management on OpenWrt
Filename: luci-app-picobrew_1.0.0-r1_all.ipk Filename: luci-app-picobrew_1.0.0-r1_all.ipk
Size: 9972 Size: 9979
Package: luci-app-secubox Package: luci-app-secubox
Version: 0.7.1-r4 Version: 0.7.1-r4
@ -356,7 +356,7 @@ Architecture: all
Installed-Size: 266240 Installed-Size: 266240
Description: Central control hub for all SecuBox modules. Provides unified dashboard, module status, system health monitoring, and quick actions. 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 Filename: luci-app-secubox_0.7.1-r4_all.ipk
Size: 49900 Size: 49901
Package: luci-app-secubox-admin Package: luci-app-secubox-admin
Version: 1.0.0-r19 Version: 1.0.0-r19
@ -367,7 +367,7 @@ Architecture: all
Installed-Size: 337920 Installed-Size: 337920
Description: Unified admin control center for SecuBox appstore plugins with system monitoring Description: Unified admin control center for SecuBox appstore plugins with system monitoring
Filename: luci-app-secubox-admin_1.0.0-r19_all.ipk Filename: luci-app-secubox-admin_1.0.0-r19_all.ipk
Size: 57094 Size: 57098
Package: luci-app-secubox-crowdsec Package: luci-app-secubox-crowdsec
Version: 1.0.0-r3 Version: 1.0.0-r3
@ -379,7 +379,7 @@ Architecture: all
Installed-Size: 81920 Installed-Size: 81920
Description: LuCI SecuBox CrowdSec Dashboard Description: LuCI SecuBox CrowdSec Dashboard
Filename: luci-app-secubox-crowdsec_1.0.0-r3_all.ipk Filename: luci-app-secubox-crowdsec_1.0.0-r3_all.ipk
Size: 13914 Size: 13925
Package: luci-app-secubox-netdiag Package: luci-app-secubox-netdiag
Version: 1.0.0-r1 Version: 1.0.0-r1
@ -391,7 +391,7 @@ Architecture: all
Installed-Size: 61440 Installed-Size: 61440
Description: Real-time DSA switch port statistics, error monitoring, and network health diagnostics Description: Real-time DSA switch port statistics, error monitoring, and network health diagnostics
Filename: luci-app-secubox-netdiag_1.0.0-r1_all.ipk Filename: luci-app-secubox-netdiag_1.0.0-r1_all.ipk
Size: 12000 Size: 12002
Package: luci-app-secubox-netifyd Package: luci-app-secubox-netifyd
Version: 1.2.1-r1 Version: 1.2.1-r1
@ -415,7 +415,7 @@ Architecture: all
Installed-Size: 215040 Installed-Size: 215040
Description: LuCI SecuBox P2P Hub Description: LuCI SecuBox P2P Hub
Filename: luci-app-secubox-p2p_0.1.0-r1_all.ipk Filename: luci-app-secubox-p2p_0.1.0-r1_all.ipk
Size: 39254 Size: 39257
Package: luci-app-secubox-portal Package: luci-app-secubox-portal
Version: 0.7.0-r2 Version: 0.7.0-r2
@ -427,7 +427,7 @@ Architecture: all
Installed-Size: 122880 Installed-Size: 122880
Description: Unified entry point for all SecuBox applications with tabbed navigation Description: Unified entry point for all SecuBox applications with tabbed navigation
Filename: luci-app-secubox-portal_0.7.0-r2_all.ipk Filename: luci-app-secubox-portal_0.7.0-r2_all.ipk
Size: 24553 Size: 24557
Package: luci-app-secubox-security-threats Package: luci-app-secubox-security-threats
Version: 1.0.0-r4 Version: 1.0.0-r4
@ -439,7 +439,7 @@ Architecture: all
Installed-Size: 71680 Installed-Size: 71680
Description: Unified dashboard integrating netifyd DPI threats with CrowdSec intelligence for real-time threat monitoring and automated blocking 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 Filename: luci-app-secubox-security-threats_1.0.0-r4_all.ipk
Size: 13899 Size: 13906
Package: luci-app-service-registry Package: luci-app-service-registry
Version: 1.0.0-r1 Version: 1.0.0-r1
@ -451,7 +451,7 @@ Architecture: all
Installed-Size: 194560 Installed-Size: 194560
Description: Unified service aggregation with HAProxy vhosts, Tor hidden services, and QR-coded landing page 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 Filename: luci-app-service-registry_1.0.0-r1_all.ipk
Size: 39826 Size: 39827
Package: luci-app-streamlit Package: luci-app-streamlit
Version: 1.0.0-r9 Version: 1.0.0-r9
@ -463,7 +463,7 @@ Architecture: all
Installed-Size: 122880 Installed-Size: 122880
Description: Modern dashboard for Streamlit Platform management on OpenWrt Description: Modern dashboard for Streamlit Platform management on OpenWrt
Filename: luci-app-streamlit_1.0.0-r9_all.ipk Filename: luci-app-streamlit_1.0.0-r9_all.ipk
Size: 20470 Size: 20471
Package: luci-app-system-hub Package: luci-app-system-hub
Version: 0.5.1-r4 Version: 0.5.1-r4
@ -475,7 +475,7 @@ Architecture: all
Installed-Size: 358400 Installed-Size: 358400
Description: Central system control with monitoring, services, logs, and backup Description: Central system control with monitoring, services, logs, and backup
Filename: luci-app-system-hub_0.5.1-r4_all.ipk Filename: luci-app-system-hub_0.5.1-r4_all.ipk
Size: 66345 Size: 66351
Package: luci-app-tor-shield Package: luci-app-tor-shield
Version: 1.0.0-r10 Version: 1.0.0-r10
@ -487,7 +487,7 @@ Architecture: all
Installed-Size: 133120 Installed-Size: 133120
Description: Modern dashboard for Tor anonymization on OpenWrt Description: Modern dashboard for Tor anonymization on OpenWrt
Filename: luci-app-tor-shield_1.0.0-r10_all.ipk Filename: luci-app-tor-shield_1.0.0-r10_all.ipk
Size: 24532 Size: 24537
Package: luci-app-traffic-shaper Package: luci-app-traffic-shaper
Version: 0.4.0-r2 Version: 0.4.0-r2
@ -499,7 +499,7 @@ Architecture: all
Installed-Size: 92160 Installed-Size: 92160
Description: Advanced traffic shaping with TC/CAKE for precise bandwidth control Description: Advanced traffic shaping with TC/CAKE for precise bandwidth control
Filename: luci-app-traffic-shaper_0.4.0-r2_all.ipk Filename: luci-app-traffic-shaper_0.4.0-r2_all.ipk
Size: 15635 Size: 15637
Package: luci-app-vhost-manager Package: luci-app-vhost-manager
Version: 0.5.0-r5 Version: 0.5.0-r5
@ -511,7 +511,7 @@ Architecture: all
Installed-Size: 153600 Installed-Size: 153600
Description: Nginx reverse proxy manager with Let's Encrypt SSL certificates, authentication, and WebSocket support 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 Filename: luci-app-vhost-manager_0.5.0-r5_all.ipk
Size: 26199 Size: 26204
Package: luci-app-wireguard-dashboard Package: luci-app-wireguard-dashboard
Version: 0.7.0-r5 Version: 0.7.0-r5
@ -523,7 +523,7 @@ Architecture: all
Installed-Size: 235520 Installed-Size: 235520
Description: Modern dashboard for WireGuard VPN monitoring on OpenWrt Description: Modern dashboard for WireGuard VPN monitoring on OpenWrt
Filename: luci-app-wireguard-dashboard_0.7.0-r5_all.ipk Filename: luci-app-wireguard-dashboard_0.7.0-r5_all.ipk
Size: 45368 Size: 45372
Package: luci-app-zigbee2mqtt Package: luci-app-zigbee2mqtt
Version: 1.0.0-r2 Version: 1.0.0-r2
@ -535,7 +535,7 @@ Architecture: all
Installed-Size: 40960 Installed-Size: 40960
Description: Graphical interface for managing the Zigbee2MQTT docker application. Description: Graphical interface for managing the Zigbee2MQTT docker application.
Filename: luci-app-zigbee2mqtt_1.0.0-r2_all.ipk Filename: luci-app-zigbee2mqtt_1.0.0-r2_all.ipk
Size: 7085 Size: 7095
Package: luci-theme-secubox Package: luci-theme-secubox
Version: 0.4.7-r1 Version: 0.4.7-r1
@ -547,7 +547,7 @@ Architecture: all
Installed-Size: 460800 Installed-Size: 460800
Description: Global CyberMood design system (CSS/JS/i18n) shared by all SecuBox dashboards. Description: Global CyberMood design system (CSS/JS/i18n) shared by all SecuBox dashboards.
Filename: luci-theme-secubox_0.4.7-r1_all.ipk Filename: luci-theme-secubox_0.4.7-r1_all.ipk
Size: 111793 Size: 111798
Package: secubox-app Package: secubox-app
Version: 1.0.0-r2 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 Description: Command line helper for SecuBox App Store manifests. Installs /usr/sbin/secubox-app
and ships the default manifests under /usr/share/secubox/plugins/. and ships the default manifests under /usr/share/secubox/plugins/.
Filename: secubox-app_1.0.0-r2_all.ipk Filename: secubox-app_1.0.0-r2_all.ipk
Size: 11185 Size: 11181
Package: secubox-app-adguardhome Package: secubox-app-adguardhome
Version: 1.0.0-r2 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 inside Docker on SecuBox-powered OpenWrt systems. Network-wide ad blocker
with DNS-over-HTTPS/TLS support and detailed analytics. with DNS-over-HTTPS/TLS support and detailed analytics.
Filename: secubox-app-adguardhome_1.0.0-r2_all.ipk Filename: secubox-app-adguardhome_1.0.0-r2_all.ipk
Size: 2876 Size: 2883
Package: secubox-app-auth-logger Package: secubox-app-auth-logger
Version: 1.2.2-r1 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 - JavaScript hook to intercept login failures
- CrowdSec parser and bruteforce scenario - CrowdSec parser and bruteforce scenario
Filename: secubox-app-auth-logger_1.2.2-r1_all.ipk Filename: secubox-app-auth-logger_1.2.2-r1_all.ipk
Size: 9378 Size: 9379
Package: secubox-app-crowdsec-custom Package: secubox-app-crowdsec-custom
Version: 1.1.0-r1 Version: 1.1.0-r1
@ -613,7 +613,7 @@ Description: Custom CrowdSec configurations for SecuBox web interface protectio
- Webapp generic auth bruteforce protection - Webapp generic auth bruteforce protection
- Whitelist for trusted networks - Whitelist for trusted networks
Filename: secubox-app-crowdsec-custom_1.1.0-r1_all.ipk Filename: secubox-app-crowdsec-custom_1.1.0-r1_all.ipk
Size: 5759 Size: 5767
Package: secubox-app-cs-firewall-bouncer Package: secubox-app-cs-firewall-bouncer
Version: 0.0.31-r4 Version: 0.0.31-r4
@ -640,7 +640,7 @@ Description: SecuBox CrowdSec Firewall Bouncer for OpenWrt.
- Automatic restart on firewall reload - Automatic restart on firewall reload
- procd service management - procd service management
Filename: secubox-app-cs-firewall-bouncer_0.0.31-r4_aarch64_cortex-a72.ipk Filename: secubox-app-cs-firewall-bouncer_0.0.31-r4_aarch64_cortex-a72.ipk
Size: 5049321 Size: 5049326
Package: secubox-app-cyberfeed Package: secubox-app-cyberfeed
Version: 0.2.1-r1 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 Features emoji injection, neon styling, and RSS-Bridge support
for social media feeds (Facebook, Twitter, Mastodon). for social media feeds (Facebook, Twitter, Mastodon).
Filename: secubox-app-cyberfeed_0.2.1-r1_all.ipk Filename: secubox-app-cyberfeed_0.2.1-r1_all.ipk
Size: 12451 Size: 12453
Package: secubox-app-domoticz Package: secubox-app-domoticz
Version: 1.0.0-r2 Version: 1.0.0-r2
@ -667,7 +667,7 @@ Installed-Size: 10240
Description: Installer, configuration, and service manager for running Domoticz Description: Installer, configuration, and service manager for running Domoticz
inside Docker on SecuBox-powered OpenWrt systems. inside Docker on SecuBox-powered OpenWrt systems.
Filename: secubox-app-domoticz_1.0.0-r2_all.ipk Filename: secubox-app-domoticz_1.0.0-r2_all.ipk
Size: 2544 Size: 2553
Package: secubox-app-exposure Package: secubox-app-exposure
Version: 1.0.0-r1 Version: 1.0.0-r1
@ -682,7 +682,7 @@ Description: Unified service exposure manager for SecuBox.
- Dynamic Tor hidden service management - Dynamic Tor hidden service management
- HAProxy SSL reverse proxy configuration - HAProxy SSL reverse proxy configuration
Filename: secubox-app-exposure_1.0.0-r1_all.ipk Filename: secubox-app-exposure_1.0.0-r1_all.ipk
Size: 6825 Size: 6838
Package: secubox-app-gitea Package: secubox-app-gitea
Version: 1.0.0-r5 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. Runs in LXC container with Alpine Linux.
Configure in /etc/config/hexojs. Configure in /etc/config/hexojs.
Filename: secubox-app-hexojs_1.0.0-r8_all.ipk Filename: secubox-app-hexojs_1.0.0-r8_all.ipk
Size: 94934 Size: 94935
Package: secubox-app-localai Package: secubox-app-localai
Version: 2.25.0-r1 Version: 2.25.0-r1
@ -794,7 +794,7 @@ Description: LocalAI native binary package for OpenWrt.
API: http://<router-ip>:8081/v1 API: http://<router-ip>:8081/v1
Filename: secubox-app-localai_2.25.0-r1_all.ipk Filename: secubox-app-localai_2.25.0-r1_all.ipk
Size: 5712 Size: 5721
Package: secubox-app-localai-wb Package: secubox-app-localai-wb
Version: 2.25.0-r1 Version: 2.25.0-r1
@ -818,7 +818,7 @@ Description: LocalAI native binary package for OpenWrt.
API: http://<router-ip>:8080/v1 API: http://<router-ip>:8080/v1
Filename: secubox-app-localai-wb_2.25.0-r1_all.ipk Filename: secubox-app-localai-wb_2.25.0-r1_all.ipk
Size: 7950 Size: 7956
Package: secubox-app-lyrion Package: secubox-app-lyrion
Version: 2.0.2-r1 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. Auto-detects available runtime, preferring LXC for lower resource usage.
Configure runtime in /etc/config/lyrion. Configure runtime in /etc/config/lyrion.
Filename: secubox-app-lyrion_2.0.2-r1_all.ipk Filename: secubox-app-lyrion_2.0.2-r1_all.ipk
Size: 7285 Size: 7289
Package: secubox-app-magicmirror2 Package: secubox-app-magicmirror2
Version: 0.4.0-r8 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. Runs in LXC container for isolation and security.
Configure in /etc/config/magicmirror2. Configure in /etc/config/magicmirror2.
Filename: secubox-app-magicmirror2_0.4.0-r8_all.ipk Filename: secubox-app-magicmirror2_0.4.0-r8_all.ipk
Size: 9251 Size: 9249
Package: secubox-app-mailinabox Package: secubox-app-mailinabox
Version: 2.0.0-r1 Version: 2.0.0-r1
@ -885,7 +885,7 @@ Description: Complete email server solution using docker-mailserver for SecuBox
Commands: mailinaboxctl --help Commands: mailinaboxctl --help
Filename: secubox-app-mailinabox_2.0.0-r1_all.ipk Filename: secubox-app-mailinabox_2.0.0-r1_all.ipk
Size: 7566 Size: 7575
Package: secubox-app-metabolizer Package: secubox-app-metabolizer
Version: 1.0.0-r3 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 Pipeline: Edit in Streamlit -> Push to Gitea -> Build with Hexo -> Publish
Filename: secubox-app-metabolizer_1.0.0-r3_all.ipk Filename: secubox-app-metabolizer_1.0.0-r3_all.ipk
Size: 13979 Size: 13984
Package: secubox-app-mitmproxy Package: secubox-app-mitmproxy
Version: 0.4.0-r16 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. Runs in LXC container for isolation and security.
Configure in /etc/config/mitmproxy. Configure in /etc/config/mitmproxy.
Filename: secubox-app-mitmproxy_0.4.0-r16_all.ipk Filename: secubox-app-mitmproxy_0.4.0-r16_all.ipk
Size: 10215 Size: 10213
Package: secubox-app-mmpm Package: secubox-app-mmpm
Version: 0.2.0-r5 Version: 0.2.0-r5
@ -948,7 +948,7 @@ Description: MMPM (MagicMirror Package Manager) for SecuBox.
Runs inside the MagicMirror2 LXC container. Runs inside the MagicMirror2 LXC container.
Filename: secubox-app-mmpm_0.2.0-r5_all.ipk Filename: secubox-app-mmpm_0.2.0-r5_all.ipk
Size: 3974 Size: 3977
Package: secubox-app-nextcloud Package: secubox-app-nextcloud
Version: 1.0.0-r2 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 inside Docker on SecuBox-powered OpenWrt systems. Self-hosted file
sync and share with calendar, contacts, and collaboration. sync and share with calendar, contacts, and collaboration.
Filename: secubox-app-nextcloud_1.0.0-r2_all.ipk Filename: secubox-app-nextcloud_1.0.0-r2_all.ipk
Size: 2955 Size: 2959
Package: secubox-app-ollama Package: secubox-app-ollama
Version: 0.1.0-r1 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. Runs in Docker/Podman container.
Configure in /etc/config/ollama. Configure in /etc/config/ollama.
Filename: secubox-app-ollama_0.1.0-r1_all.ipk Filename: secubox-app-ollama_0.1.0-r1_all.ipk
Size: 5733 Size: 5739
Package: secubox-app-picobrew Package: secubox-app-picobrew
Version: 1.0.0-r7 Version: 1.0.0-r7
@ -1033,7 +1033,7 @@ Description: Streamlit App Platform - Self-hosted Python data app platform
Configure in /etc/config/streamlit. Configure in /etc/config/streamlit.
Filename: secubox-app-streamlit_1.0.0-r5_all.ipk Filename: secubox-app-streamlit_1.0.0-r5_all.ipk
Size: 11717 Size: 11721
Package: secubox-app-tor Package: secubox-app-tor
Version: 1.0.0-r1 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. Configure in /etc/config/tor-shield.
Filename: secubox-app-tor_1.0.0-r1_all.ipk Filename: secubox-app-tor_1.0.0-r1_all.ipk
Size: 7379 Size: 7377
Package: secubox-app-webapp Package: secubox-app-webapp
Version: 1.5.0-r7 Version: 1.5.0-r7
@ -1074,7 +1074,7 @@ Description: SecuBox Control Center Dashboard - A web-based dashboard for monit
- Service management - Service management
- Network interface control - Network interface control
Filename: secubox-app-webapp_1.5.0-r7_all.ipk Filename: secubox-app-webapp_1.5.0-r7_all.ipk
Size: 39169 Size: 39171
Package: secubox-app-zigbee2mqtt Package: secubox-app-zigbee2mqtt
Version: 1.0.0-r3 Version: 1.0.0-r3
@ -1087,7 +1087,7 @@ Installed-Size: 20480
Description: Installer, configuration, and service manager for running Zigbee2MQTT Description: Installer, configuration, and service manager for running Zigbee2MQTT
inside Docker on SecuBox-powered OpenWrt systems. inside Docker on SecuBox-powered OpenWrt systems.
Filename: secubox-app-zigbee2mqtt_1.0.0-r3_all.ipk Filename: secubox-app-zigbee2mqtt_1.0.0-r3_all.ipk
Size: 3542 Size: 3545
Package: secubox-core Package: secubox-core
Version: 0.10.0-r9 Version: 0.10.0-r9
@ -1107,21 +1107,22 @@ Description: SecuBox Core Framework provides the foundational infrastructure fo
- Unified CLI interface - Unified CLI interface
- ubus RPC backend - ubus RPC backend
Filename: secubox-core_0.10.0-r9_all.ipk Filename: secubox-core_0.10.0-r9_all.ipk
Size: 80068 Size: 80071
Package: secubox-p2p Package: secubox-p2p
Version: 0.4.0-r1 Version: 0.5.0-r1
Depends: jsonfilter, curl, avahi-daemon, avahi-utils, uhttpd Depends: jsonfilter, curl, avahi-daemon, avahi-utils, uhttpd
License: MIT License: MIT
Section: secubox Section: secubox
Maintainer: SecuBox Team Maintainer: SecuBox Team
Architecture: all Architecture: all
Installed-Size: 133120 Installed-Size: 143360
Description: SecuBox P2P Hub backend providing peer discovery, mesh networking Description: SecuBox P2P Hub backend providing peer discovery, mesh networking
DNS federation, and distributed service management. Includes mDNS DNS federation, and distributed service management. Includes mDNS
service announcement, REST API on port 7331 for mesh visibility service announcement, REST API on port 7331 for mesh visibility
and SecuBox Factory unified dashboard with Ed25519 signed Merkle SecuBox Factory unified dashboard with Ed25519 signed Merkle
snapshots for cryptographic configuration validation. snapshots for cryptographic configuration validation, and distributed
Filename: secubox-p2p_0.4.0-r1_all.ipk mesh services panel for aggregated service discovery across all nodes.
Size: 27891 Filename: secubox-p2p_0.5.0-r1_all.ipk
Size: 30574

View File

@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-p2p PKG_NAME:=secubox-p2p
PKG_VERSION:=0.5.0 PKG_VERSION:=0.6.0
PKG_RELEASE:=1 PKG_RELEASE:=1
PKG_MAINTAINER:=SecuBox Team PKG_MAINTAINER:=SecuBox Team
@ -22,8 +22,10 @@ define Package/secubox-p2p/description
DNS federation, and distributed service management. Includes mDNS DNS federation, and distributed service management. Includes mDNS
service announcement, REST API on port 7331 for mesh visibility, service announcement, REST API on port 7331 for mesh visibility,
SecuBox Factory unified dashboard with Ed25519 signed Merkle SecuBox Factory unified dashboard with Ed25519 signed Merkle
snapshots for cryptographic configuration validation, and distributed snapshots for cryptographic configuration validation, distributed
mesh services panel for aggregated service discovery across all nodes. 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 endef
define Package/secubox-p2p/conffiles 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/snapshot $(1)/www/api/factory/
$(INSTALL_BIN) ./root/www/api/factory/pubkey $(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/mesh-services $(1)/www/api/factory/
$(INSTALL_BIN) ./root/www/api/factory/catalog $(1)/www/api/factory/
# Factory Web UI # Factory Web UI
$(INSTALL_DIR) $(1)/www/factory $(INSTALL_DIR) $(1)/www/factory

View File

@ -295,6 +295,177 @@ factory_audit_log() {
fi 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 # Main entry point for CLI usage
case "${1:-}" in case "${1:-}" in
init) init)
@ -331,6 +502,33 @@ case "${1:-}" in
merkle) merkle)
merkle_config 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 # Sourced as library - do nothing
: :

View File

@ -3,7 +3,7 @@
# Handles peer discovery, mesh networking, and service federation # Handles peer discovery, mesh networking, and service federation
# Supports WAN IP, LAN IP, and WireGuard tunnel redundancy # Supports WAN IP, LAN IP, and WireGuard tunnel redundancy
VERSION="0.4.0" VERSION="0.6.0"
CONFIG_FILE="/etc/config/secubox-p2p" CONFIG_FILE="/etc/config/secubox-p2p"
PEERS_FILE="/tmp/secubox-p2p-peers.json" PEERS_FILE="/tmp/secubox-p2p-peers.json"
SERVICES_FILE="/tmp/secubox-p2p-services.json" SERVICES_FILE="/tmp/secubox-p2p-services.json"
@ -439,6 +439,210 @@ stop_mdns() {
fi 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) # Get node status JSON (for REST API)
# Now includes WAN IP and WireGuard tunnel addresses # Now includes WAN IP and WireGuard tunnel addresses
get_node_status() { get_node_status() {
@ -526,6 +730,9 @@ daemon_loop() {
# Register self in peer list # Register self in peer list
register_self register_self
# Initialize DNS federation
dns_init
# Setup signal handlers # Setup signal handlers
trap 'stop_mdns; exit 0' INT TERM trap 'stop_mdns; exit 0' INT TERM
@ -603,6 +810,9 @@ daemon_loop() {
echo "$peers" > "$PEERS_FILE" echo "$peers" > "$PEERS_FILE"
fi fi
# Update DNS federation with current peer list
dns_update_peers >/dev/null 2>&1
# Sleep interval # Sleep interval
local interval=$(get_config main sync_interval 60) local interval=$(get_config main sync_interval 60)
sleep "$interval" sleep "$interval"
@ -660,8 +870,28 @@ case "$1" in
init init
register_self 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 exit 1
;; ;;
esac esac

View 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

View File

@ -137,6 +137,61 @@ case "$tool_id" in
output="Merkle root: $merkle" 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" output="Unknown tool: $tool_id"
success=0 success=0

View File

@ -112,15 +112,73 @@ cat << 'EOF'
"category": "security", "category": "security",
"icon": "hash", "icon": "hash",
"dangerous": false "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": [ "categories": [
{"id": "security", "name": "Security", "order": 1}, {"id": "security", "name": "Security", "order": 1},
{"id": "mesh", "name": "Mesh Network", "order": 2}, {"id": "mesh", "name": "Mesh Network", "order": 2},
{"id": "monitoring", "name": "Monitoring", "order": 3}, {"id": "dns", "name": "DNS Federation", "order": 3},
{"id": "maintenance", "name": "Maintenance", "order": 4}, {"id": "catalog", "name": "Catalog", "order": 4},
{"id": "backup", "name": "Backup", "order": 5}, {"id": "monitoring", "name": "Monitoring", "order": 5},
{"id": "queue", "name": "Queue", "order": 6} {"id": "maintenance", "name": "Maintenance", "order": 6},
{"id": "backup", "name": "Backup", "order": 7},
{"id": "queue", "name": "Queue", "order": 8}
] ]
} }
EOF EOF

View File

@ -98,12 +98,12 @@
/* Fingerprint display */ /* 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; } .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 */ /* Emoji nav */
.tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; } .emoji-nav { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.tab { padding: 0.5rem 1rem; background: transparent; border: none; color: var(--muted); cursor: pointer; font-weight: 500; position: relative; } .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; }
.tab.active { color: var(--accent); } .emoji-btn:hover { filter: grayscale(0); opacity: 1; transform: scale(1.1); }
.tab.active::after { content: ''; position: absolute; bottom: -0.5rem; left: 0; right: 0; height: 2px; background: var(--accent); } .emoji-btn.active { filter: grayscale(0); opacity: 1; border-color: var(--accent); background: rgba(99, 102, 241, 0.2); }
.tab:hover { color: var(--text); } .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 { display: none; }
.tab-content.active { display: block; } .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 { 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-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; } .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> </style>
</head> </head>
<body> <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> <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 SecuBox Factory
</div> </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"> <div class="header-right">
<span class="badge badge-muted" id="node-count">- nodes</span> <span class="badge badge-muted" id="node-count">- nodes</span>
<span class="badge" id="snapshot-status">...</span> <span class="badge" id="snapshot-status">...</span>
<span class="fingerprint" id="fingerprint" title="Node Fingerprint">...</span> <span class="fingerprint" id="fingerprint" title="Node Fingerprint">...</span>
<button onclick="toggleTools()" title="Tools">Tools</button> <button onclick="toggleTools()" title="Tools">🧰</button>
</div> </div>
</header> </header>
<main style="padding: 1rem;"> <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 id="dashboard" class="tab-content active" style="display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));">
<div class="card accent"> <div class="card accent">
@ -170,6 +243,12 @@
<div class="stat"><div class="spinner"></div><div class="stat-label">Loading services...</div></div> <div class="stat"><div class="spinner"></div><div class="stat-label">Loading services...</div></div>
</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> </main>
<div class="overlay" id="overlay" onclick="toggleTools()"></div> <div class="overlay" id="overlay" onclick="toggleTools()"></div>
@ -187,14 +266,27 @@
</div> </div>
</dialog> </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;">&times;</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> <script>
// State // State
let data = { local: {}, peers: [], snapshot: {}, services: {}, system: {} }; let data = { local: {}, peers: [], snapshot: {}, services: {}, system: {} };
let meshServices = { nodes: [], summary: {} }; let meshServices = { nodes: [], summary: {} };
let catalogData = { services: [], node_name: '', updated: '' };
let tools = []; let tools = [];
let refreshInterval = null; let refreshInterval = null;
let activeTab = 'dashboard'; let activeTab = 'dashboard';
let serviceFilter = { search: '', status: 'all', node: 'all' }; 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) // API helpers - use same origin (HAProxy routes /factory/* to API)
const apiBase = '/factory/'; 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 // Switch tabs
function switchTab(tab) { function switchTab(tab) {
activeTab = 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.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'); document.getElementById(tab).classList.add('active');
if (tab === 'services') { if (tab === 'services') {
refreshMeshServices(); 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 // Filter services
function filterServices(type, value) { function filterServices(type, value) {
serviceFilter[type] = value; serviceFilter[type] = value;
@ -472,19 +592,20 @@
html += `<div style="padding: 1rem; text-align: center; color: var(--muted);">No matching services</div>`; html += `<div style="padding: 1rem; text-align: center; color: var(--muted);">No matching services</div>`;
} else { } else {
services.forEach(svc => { services.forEach(svc => {
const statusClass = svc.status === 'running' ? 'running' : (svc.enabled ? 'stopped' : 'disabled');
const hasPort = svc.port && svc.port !== '' && svc.port !== '0'; 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 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 += ` html += `
<div class="service-item"> <div class="service-item">
<div class="service-info"> <div class="service-info">
<div class="service-name">${svc.name}</div> <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>
<div class="service-status"> <div class="service-status">
${accessUrl ? `<a href="${accessUrl}" target="_blank" class="service-link">Open</a>` : ''} ${accessUrl ? `<span class="health-emoji clickable" onclick="showQR('${svc.name}', '${accessUrl}')" title="Show QR">📱</span>` : ''}
<span class="status-dot ${statusClass}" title="${svc.status}"></span> <span class="health-emoji" title="${healthTitle}" onclick="toggleService('${svc.name}', '${svc.status}')">${healthEmoji}</span>
</div> </div>
</div>`; </div>`;
}); });
@ -500,6 +621,262 @@
document.getElementById('services').innerHTML = html; 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, '&quot;')}, '${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 // Render tools panel
function renderTools(categories) { function renderTools(categories) {
const grouped = {}; const grouped = {};
@ -559,10 +936,51 @@
document.getElementById('modal').close(); 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 // Handle ESC key
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { 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(); else if (document.getElementById('tools').classList.contains('open')) toggleTools();
} }
}); });