diff --git a/CLAUDE.md b/CLAUDE.md index a1b1bd99..6abc38cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,24 @@ - `ss` command may not be available - use `netstat` or `/proc/net/tcp` as fallbacks - `sqlite3` may not be installed - provide fallback methods (e.g., delete database file instead of running SQL) +### JSON Parsing +- **Use `jsonfilter` instead of `jq`** - jsonfilter is native to OpenWrt (part of libubox), jq is often not installed +- Syntax examples: + ```bash + # Get a field value + jsonfilter -i /path/to/file.json -e '@.field_name' + + # Get nested field + jsonfilter -i /path/to/file.json -e '@.parent.child' + + # Get array length (count elements) + jsonfilter -i /path/to/file.json -e '@[*]' | wc -l + + # Get array element + jsonfilter -i /path/to/file.json -e '@[0]' + ``` +- Always check for empty results: `[ -z "$result" ] && result=0` + ### Port Detection When checking if a port is listening, use this order of fallbacks: 1. `/proc/net/tcp` (always available) - ports are in hex (e.g., 8080 = 1F90) diff --git a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/api.js b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/api.js index 2656fd55..4d3e84b4 100644 --- a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/api.js +++ b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/api.js @@ -11,7 +11,7 @@ var callStatus = rpc.declare({ var callGetActiveStreams = rpc.declare({ object: 'luci.media-flow', method: 'get_active_streams', - expect: { streams: [] } + expect: { } }); var callGetStreamHistory = rpc.declare({ diff --git a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/alerts.js b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/alerts.js index e5983e1c..d76c84bc 100644 --- a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/alerts.js +++ b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/alerts.js @@ -7,6 +7,18 @@ 'require secubox-portal/header as SbHeader'; return L.view.extend({ + // Initialize SecuBox dark theme + initTheme: function() { + document.documentElement.setAttribute('data-theme', 'dark'); + document.body.classList.add('secubox-mode'); + if (!document.getElementById('mf-theme-styles')) { + var themeStyle = document.createElement('style'); + themeStyle.id = 'mf-theme-styles'; + themeStyle.textContent = 'body.secubox-mode { background: #0a0a0f !important; } body.secubox-mode .main-right, body.secubox-mode #maincontent, body.secubox-mode .container { background: transparent !important; }'; + document.head.appendChild(themeStyle); + } + }, + load: function() { return Promise.all([ API.listAlerts() @@ -14,6 +26,9 @@ return L.view.extend({ }, render: function(data) { + var self = this; + this.initTheme(); + var alerts = data[0] || []; var m = new form.Map('media_flow', null, null); diff --git a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/clients.js b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/clients.js index 487618fb..988de525 100644 --- a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/clients.js +++ b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/clients.js @@ -6,6 +6,18 @@ 'require secubox-portal/header as SbHeader'; return L.view.extend({ + // Initialize SecuBox dark theme + initTheme: function() { + document.documentElement.setAttribute('data-theme', 'dark'); + document.body.classList.add('secubox-mode'); + if (!document.getElementById('mf-theme-styles')) { + var themeStyle = document.createElement('style'); + themeStyle.id = 'mf-theme-styles'; + themeStyle.textContent = 'body.secubox-mode { background: #0a0a0f !important; } body.secubox-mode .main-right, body.secubox-mode #maincontent, body.secubox-mode .container { background: transparent !important; }'; + document.head.appendChild(themeStyle); + } + }, + load: function() { return Promise.all([ API.getStatsByClient() @@ -13,6 +25,8 @@ return L.view.extend({ }, render: function(data) { + this.initTheme(); + var statsByClient = data[0] || {}; var clients = statsByClient.clients || {}; diff --git a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/dashboard.js b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/dashboard.js index ebbf67c6..e83f2f02 100644 --- a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/dashboard.js +++ b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/dashboard.js @@ -10,6 +10,26 @@ return view.extend({ title: _('Media Flow Dashboard'), pollInterval: 5, + // Initialize SecuBox dark theme + initTheme: function() { + // Set dark theme on document + document.documentElement.setAttribute('data-theme', 'dark'); + document.body.classList.add('secubox-mode'); + + // Apply dark background to body for SecuBox styling + if (!document.getElementById('mf-theme-styles')) { + var themeStyle = document.createElement('style'); + themeStyle.id = 'mf-theme-styles'; + themeStyle.textContent = ` + body.secubox-mode { background: #0a0a0f !important; } + body.secubox-mode .main-right, + body.secubox-mode #maincontent, + body.secubox-mode .container { background: transparent !important; } + `; + document.head.appendChild(themeStyle); + } + }, + formatBytes: function(bytes) { if (bytes === 0) return '0 B'; var k = 1024; @@ -28,6 +48,10 @@ return view.extend({ render: function(data) { var self = this; + + // Initialize SecuBox dark theme + this.initTheme(); + var status = data[0] || {}; var streamsData = data[1] || {}; var statsByService = data[2] || {}; @@ -36,7 +60,7 @@ return view.extend({ var isNdpid = dpiSource === 'ndpid'; var isNetifyd = dpiSource === 'netifyd'; var streams = streamsData.streams || []; - var flowCount = streamsData.flow_count || status.active_flows || 0; + var flowCount = streamsData.flow_count || status.active_flows || status.ndpid_flows || 0; // Inject CSS var css = ` diff --git a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/history.js b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/history.js index 27ee6c2e..27bcc781 100644 --- a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/history.js +++ b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/history.js @@ -6,6 +6,18 @@ 'require secubox-portal/header as SbHeader'; return L.view.extend({ + // Initialize SecuBox dark theme + initTheme: function() { + document.documentElement.setAttribute('data-theme', 'dark'); + document.body.classList.add('secubox-mode'); + if (!document.getElementById('mf-theme-styles')) { + var themeStyle = document.createElement('style'); + themeStyle.id = 'mf-theme-styles'; + themeStyle.textContent = 'body.secubox-mode { background: #0a0a0f !important; } body.secubox-mode .main-right, body.secubox-mode #maincontent, body.secubox-mode .container { background: transparent !important; }'; + document.head.appendChild(themeStyle); + } + }, + load: function() { return Promise.all([ API.getStreamHistory(24) @@ -13,6 +25,8 @@ return L.view.extend({ }, render: function(data) { + this.initTheme(); + var historyData = data[0] || {}; var history = historyData.history || []; diff --git a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/services.js b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/services.js index 4e6bec7d..4a864955 100644 --- a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/services.js +++ b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/services.js @@ -6,6 +6,18 @@ 'require secubox-portal/header as SbHeader'; return L.view.extend({ + // Initialize SecuBox dark theme + initTheme: function() { + document.documentElement.setAttribute('data-theme', 'dark'); + document.body.classList.add('secubox-mode'); + if (!document.getElementById('mf-theme-styles')) { + var themeStyle = document.createElement('style'); + themeStyle.id = 'mf-theme-styles'; + themeStyle.textContent = 'body.secubox-mode { background: #0a0a0f !important; } body.secubox-mode .main-right, body.secubox-mode #maincontent, body.secubox-mode .container { background: transparent !important; }'; + document.head.appendChild(themeStyle); + } + }, + load: function() { return Promise.all([ API.getStatsByService() @@ -13,6 +25,8 @@ return L.view.extend({ }, render: function(data) { + this.initTheme(); + var statsByService = data[0] || {}; var services = statsByService.services || {}; diff --git a/package/secubox/luci-app-media-flow/root/usr/libexec/rpcd/luci.media-flow b/package/secubox/luci-app-media-flow/root/usr/libexec/rpcd/luci.media-flow index 1ba9af3e..44918488 100755 --- a/package/secubox/luci-app-media-flow/root/usr/libexec/rpcd/luci.media-flow +++ b/package/secubox/luci-app-media-flow/root/usr/libexec/rpcd/luci.media-flow @@ -24,8 +24,15 @@ init_storage() { # Detect available DPI source get_dpi_source() { - # Prefer nDPId if running and has data - if [ -f "$NDPID_FLOWS" ] && pgrep ndpid >/dev/null 2>&1; then + # Prefer nDPId if running + # Check for ndpid process and either direct flows file or compat layer output + if pgrep ndpid >/dev/null 2>&1; then + # nDPId running - check for data files + if [ -f "$NDPID_FLOWS" ] || [ -f "$MEDIA_CACHE" ] || [ -f /var/run/netifyd/status.json ]; then + echo "ndpid" + return + fi + # nDPId running but no data yet echo "ndpid" elif [ -f /var/run/netifyd/status.json ] && pgrep netifyd >/dev/null 2>&1; then echo "netifyd" @@ -124,9 +131,19 @@ case "$1" in ndpid_version="unknown" ndpid_flows=0 pgrep ndpid > /dev/null 2>&1 && ndpid_running=1 - if [ "$ndpid_running" = "1" ] && [ -f "$NDPID_FLOWS" ]; then - ndpid_flows=$(jq 'length' "$NDPID_FLOWS" 2>/dev/null || echo 0) - ndpid_version=$(ndpid -v 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+' | head -1 || echo "unknown") + if [ "$ndpid_running" = "1" ]; then + ndpid_version=$(ndpid -v 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+' | head -1) + [ -z "$ndpid_version" ] && ndpid_version="unknown" + # Check for flows from compat layer status.json + if [ -f /var/run/netifyd/status.json ] && [ -s /var/run/netifyd/status.json ]; then + # Read flow_count using jsonfilter (busybox-friendly) + ndpid_flows=$(jsonfilter -i /var/run/netifyd/status.json -e '@.flow_count' 2>/dev/null) + [ -z "$ndpid_flows" ] && ndpid_flows=$(jsonfilter -i /var/run/netifyd/status.json -e '@.flows_active' 2>/dev/null) + [ -z "$ndpid_flows" ] && ndpid_flows=0 + elif [ -f "$NDPID_FLOWS" ] && [ -s "$NDPID_FLOWS" ]; then + ndpid_flows=$(jsonfilter -i "$NDPID_FLOWS" -e '@[*]' 2>/dev/null | wc -l) + [ -z "$ndpid_flows" ] && ndpid_flows=0 + fi fi # Check netifyd status @@ -137,9 +154,12 @@ case "$1" in netifyd_flows=0 netifyd_version="unknown" - if [ "$netifyd_running" = "1" ] && [ -n "$netifyd_data" ] && [ "$netifyd_data" != "{}" ]; then - netifyd_flows=$(echo "$netifyd_data" | jq '.flows_active // .flow_count // 0' 2>/dev/null || echo 0) - netifyd_version=$(echo "$netifyd_data" | jq -r '.agent_version // "unknown"' 2>/dev/null || echo "unknown") + if [ "$netifyd_running" = "1" ] && [ -f /var/run/netifyd/status.json ] && [ -s /var/run/netifyd/status.json ]; then + netifyd_flows=$(jsonfilter -i /var/run/netifyd/status.json -e '@.flows_active' 2>/dev/null) + [ -z "$netifyd_flows" ] && netifyd_flows=$(jsonfilter -i /var/run/netifyd/status.json -e '@.flow_count' 2>/dev/null) + [ -z "$netifyd_flows" ] && netifyd_flows=0 + netifyd_version=$(jsonfilter -i /var/run/netifyd/status.json -e '@.agent_version' 2>/dev/null) + [ -z "$netifyd_version" ] && netifyd_version="unknown" fi # Determine active DPI source @@ -153,11 +173,17 @@ case "$1" in fi history_count=0 - [ -f "$HISTORY_FILE" ] && history_count=$(jq 'length' "$HISTORY_FILE" 2>/dev/null || echo 0) + if [ -f "$HISTORY_FILE" ]; then + history_count=$(jq 'length' "$HISTORY_FILE" 2>/dev/null) + [ -z "$history_count" ] && history_count=0 + fi # Check if nDPId cache has active streams active_streams=0 - [ -f "$MEDIA_CACHE" ] && active_streams=$(jq 'length' "$MEDIA_CACHE" 2>/dev/null || echo 0) + if [ -f "$MEDIA_CACHE" ]; then + active_streams=$(jq 'length' "$MEDIA_CACHE" 2>/dev/null) + [ -z "$active_streams" ] && active_streams=0 + fi enabled=$(uci -q get media_flow.global.enabled 2>/dev/null || echo "1") refresh=$(uci -q get media_flow.global.refresh_interval 2>/dev/null || echo "5") @@ -194,13 +220,22 @@ case "$1" in if [ -f "$MEDIA_CACHE" ]; then streams=$(cat "$MEDIA_CACHE" 2>/dev/null || echo "[]") fi - if [ -f "$NDPID_FLOWS" ]; then - flow_count=$(jq 'length' "$NDPID_FLOWS" 2>/dev/null || echo 0) + # Get flow count - prefer compat layer status.json + if [ -f /var/run/netifyd/status.json ] && [ -s /var/run/netifyd/status.json ]; then + flow_count=$(jsonfilter -i /var/run/netifyd/status.json -e '@.flow_count' 2>/dev/null) + [ -z "$flow_count" ] && flow_count=$(jsonfilter -i /var/run/netifyd/status.json -e '@.flows_active' 2>/dev/null) + [ -z "$flow_count" ] && flow_count=0 + elif [ -f "$NDPID_FLOWS" ] && [ -s "$NDPID_FLOWS" ]; then + flow_count=$(jsonfilter -i "$NDPID_FLOWS" -e '@[*]' 2>/dev/null | wc -l) + [ -z "$flow_count" ] && flow_count=0 fi note="Streams detected via nDPId local DPI" elif [ "$dpi_source" = "netifyd" ]; then - netifyd_data=$(get_netifyd_data) - flow_count=$(echo "$netifyd_data" | jq '.flows_active // .flow_count // 0' 2>/dev/null || echo 0) + if [ -f /var/run/netifyd/status.json ] && [ -s /var/run/netifyd/status.json ]; then + flow_count=$(jsonfilter -i /var/run/netifyd/status.json -e '@.flows_active' 2>/dev/null) + [ -z "$flow_count" ] && flow_count=$(jsonfilter -i /var/run/netifyd/status.json -e '@.flow_count' 2>/dev/null) + [ -z "$flow_count" ] && flow_count=0 + fi note="Application detection requires netifyd cloud subscription" else note="No DPI engine available"