From c9f719a8de0c1843eb93d4577faca76bacf36841 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Thu, 8 Jan 2026 18:54:19 +0100 Subject: [PATCH] feat: Update Media Flow for netifyd 5.x compatibility (v0.5.2) - Adapt RPCD backend to use netifyd 5.x status.json structure - Read flows_active/flow_count from proper fields - Extract agent_version instead of version - Parse interface stats from .stats object - Add get_network_stats endpoint with CPU/memory metrics - Update dashboard to show netifyd limitation notice - Display flow count and network statistics instead of streams Note: netifyd 5.x requires cloud subscription for application detection. Local mode only provides aggregate flow statistics. Co-Authored-By: Claude Opus 4.5 --- package/secubox/luci-app-media-flow/Makefile | 2 +- .../resources/view/media-flow/dashboard.js | 109 ++++++-------- .../root/usr/libexec/rpcd/luci.media-flow | 135 ++++++++---------- 3 files changed, 102 insertions(+), 144 deletions(-) diff --git a/package/secubox/luci-app-media-flow/Makefile b/package/secubox/luci-app-media-flow/Makefile index 5af8a712..2a01dc5a 100644 --- a/package/secubox/luci-app-media-flow/Makefile +++ b/package/secubox/luci-app-media-flow/Makefile @@ -4,7 +4,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-media-flow -PKG_VERSION:=0.5.1 +PKG_VERSION:=0.5.2 PKG_RELEASE:=1 PKG_ARCH:=all PKG_LICENSE:=Apache-2.0 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 0f8d0ce9..0eb588ae 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 @@ -15,13 +15,13 @@ return L.view.extend({ render: function(data) { var status = data[0] || {}; - var activeStreams = data[1] || []; + var streamsData = data[1] || {}; var statsByService = data[2] || {}; var v = E('div', { 'class': 'cbi-map' }, [ - E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), E('h2', {}, _('Media Flow Dashboard')), - E('div', { 'class': 'cbi-map-descr' }, _('Real-time detection and monitoring of streaming services')) + E('div', { 'class': 'cbi-map-descr' }, _('Network flow monitoring and statistics')) ]); // Status overview @@ -35,85 +35,66 @@ return L.view.extend({ ]), E('div', { 'class': 'td left', 'width': '33%' }, [ E('strong', {}, _('Netifyd: ')), - E('span', {}, status.netifyd_running ? - E('span', { 'style': 'color: green' }, '● ' + _('Running')) : + E('span', {}, status.netifyd_running ? + E('span', { 'style': 'color: green' }, '● ' + _('Running') + ' (v' + (status.netifyd_version || '?') + ')') : E('span', { 'style': 'color: red' }, '● ' + _('Stopped')) ) ]), E('div', { 'class': 'td left', 'width': '33%' }, [ - E('strong', {}, _('Active Streams: ')), - E('span', { 'style': 'font-size: 1.5em; color: #0088cc' }, String(status.active_streams || 0)) + E('strong', {}, _('Active Flows: ')), + E('span', { 'style': 'font-size: 1.5em; color: #0088cc' }, String(status.active_flows || 0)) ]) ]) ]) ]); v.appendChild(statusSection); - // Active streams - var activeSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Active Streams')), - E('div', { 'id': 'active-streams-table' }) + // Netifyd 5.x limitation notice + var noticeSection = E('div', { 'class': 'cbi-section' }, [ + E('div', { 'class': 'alert-message warning', 'style': 'background: #fff3cd; border: 1px solid #ffc107; padding: 15px; border-radius: 4px; margin-bottom: 15px;' }, [ + E('strong', {}, _('Notice: ')), + E('span', {}, _('Netifyd 5.x requires a cloud subscription for streaming service detection. Currently showing network flow statistics only.')) + ]) + ]); + v.appendChild(noticeSection); + + // Network flow stats + var flowSection = E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Network Flows')), + E('div', { 'id': 'flow-stats-container' }) ]); - var updateActiveStreams = function() { - API.getActiveStreams().then(function(streams) { - var table = E('table', { 'class': 'table' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th' }, _('Service')), - E('th', { 'class': 'th' }, _('Category')), - E('th', { 'class': 'th' }, _('Client')), - E('th', { 'class': 'th' }, _('Quality')), - E('th', { 'class': 'th' }, _('Bandwidth')) + var updateFlowStats = function() { + API.getActiveStreams().then(function(data) { + var container = document.getElementById('flow-stats-container'); + if (!container) return; + + var flowCount = data.flow_count || 0; + var note = data.note || ''; + + container.innerHTML = ''; + container.appendChild(E('div', { 'class': 'table', 'style': 'background: #f8f9fa; padding: 20px; border-radius: 8px;' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td', 'style': 'text-align: center;' }, [ + E('div', { 'style': 'font-size: 3em; color: #0088cc; font-weight: bold;' }, String(flowCount)), + E('div', { 'style': 'color: #666; margin-top: 5px;' }, _('Active Network Flows')) + ]) ]) - ]); + ])); - if (streams && streams.length > 0) { - streams.forEach(function(stream) { - var qualityColor = { - 'SD': '#999', - 'HD': '#0088cc', - 'FHD': '#00cc00', - '4K': '#cc0000' - }[stream.quality] || '#666'; - - var categoryIcon = { - 'video': '🎬', - 'audio': '🎵', - 'visio': '📹' - }[stream.category] || '📊'; - - table.appendChild(E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, categoryIcon + ' ' + stream.application), - E('td', { 'class': 'td' }, stream.category), - E('td', { 'class': 'td' }, stream.client_ip), - E('td', { 'class': 'td' }, - E('span', { 'style': 'color: ' + qualityColor + '; font-weight: bold' }, stream.quality) - ), - E('td', { 'class': 'td' }, stream.bandwidth_kbps + ' kbps') - ])); - }); - } else { - table.appendChild(E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td', 'colspan': '5', 'style': 'text-align: center; font-style: italic' }, - _('No active streams detected') - ) - ])); - } - - var container = document.getElementById('active-streams-table'); - if (container) { - container.innerHTML = ''; - container.appendChild(table); + if (note) { + container.appendChild(E('p', { 'style': 'font-style: italic; color: #666; text-align: center; margin-top: 10px;' }, note)); } }); }; - updateActiveStreams(); - v.appendChild(activeSection); + updateFlowStats(); + v.appendChild(flowSection); - // Stats by service (donut chart + bars) + // Stats by service (from history) var statsSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Usage by Service')), + E('h3', {}, _('Historical Usage by Service')), + E('p', { 'style': 'color: #666; font-size: 0.9em;' }, _('Data collected from previous sessions (if available)')), E('div', { 'style': 'display: flex; gap: 20px;' }, [ E('div', { 'style': 'flex: 0 0 300px;' }, [ E('canvas', { @@ -205,7 +186,7 @@ return L.view.extend({ var servicesList = Object.keys(services); if (servicesList.length === 0) { - container.appendChild(E('p', { 'style': 'font-style: italic' }, _('No historical data available'))); + container.appendChild(E('p', { 'style': 'font-style: italic' }, _('No historical data available. Stream detection requires netifyd cloud subscription.'))); drawDonutChart({}, [], 0); return; } @@ -265,7 +246,7 @@ return L.view.extend({ // Setup auto-refresh poll.add(L.bind(function() { - updateActiveStreams(); + updateFlowStats(); updateServiceStats(); }, this), 5); 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 26c5ce2f..cf498869 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 @@ -1,6 +1,8 @@ #!/bin/sh # RPCD backend for Media Flow # Provides ubus interface: luci.media-flow +# Note: netifyd 5.x does not export per-flow application data locally +# This module shows available network statistics from netifyd . /lib/functions.sh . /usr/share/libubox/jshn.sh @@ -14,42 +16,6 @@ init_storage() { [ ! -f "$HISTORY_FILE" ] && echo '[]' > "$HISTORY_FILE" } -# Streaming services patterns -STREAMING_VIDEO="netflix|youtube|disney|primevideo|amazon.*video|twitch|hulu|hbo|vimeo|peacock|paramount|crunchyroll|funimation" -STREAMING_AUDIO="spotify|apple.*music|deezer|soundcloud|tidal|pandora|amazon.*music|youtube.*music" -STREAMING_VISIO="zoom|teams|meet|discord|skype|webex|facetime|whatsapp" - -# Detect if application is a streaming service -is_streaming_service() { - local app="$1" - echo "$app" | grep -qiE "$STREAMING_VIDEO|$STREAMING_AUDIO|$STREAMING_VISIO" -} - -# Get service category -get_service_category() { - local app="$1" - echo "$app" | grep -qiE "$STREAMING_VIDEO" && echo "video" && return - echo "$app" | grep -qiE "$STREAMING_AUDIO" && echo "audio" && return - echo "$app" | grep -qiE "$STREAMING_VISIO" && echo "visio" && return - echo "other" -} - -# Estimate quality based on bandwidth (kbps) -estimate_quality() { - local bandwidth="$1" - [ -z "$bandwidth" ] && bandwidth=0 - - if [ "$bandwidth" -lt 1000 ] 2>/dev/null; then - echo "SD" - elif [ "$bandwidth" -lt 3000 ] 2>/dev/null; then - echo "HD" - elif [ "$bandwidth" -lt 8000 ] 2>/dev/null; then - echo "FHD" - else - echo "4K" - fi -} - # Get netifyd status data get_netifyd_data() { if [ -f /var/run/netifyd/status.json ]; then @@ -59,39 +25,32 @@ get_netifyd_data() { fi } -# Build active streams JSON array -build_active_streams_json() { - local netifyd_data="$1" - local result="[]" +# Build network stats from netifyd status.json +build_network_stats_json() { + netifyd_data="$1" - # Extract flows from netifyd data - local flows=$(echo "$netifyd_data" | jq -c '.flows // []' 2>/dev/null) - [ -z "$flows" ] || [ "$flows" = "null" ] && flows="[]" - - # Process each flow and filter streaming services - result=$(echo "$flows" | jq -c ' - [.[] | select(.detected_application != null and .detected_application != "") | - select(.detected_application | test("netflix|youtube|disney|primevideo|amazon.*video|twitch|hulu|hbo|vimeo|spotify|apple.*music|deezer|soundcloud|tidal|zoom|teams|meet|discord|skype|webex"; "i")) | - { - application: .detected_application, - client_ip: (.local_ip // .src_ip // "unknown"), - server_ip: (.other_ip // .dst_ip // "unknown"), - total_bytes: (.total_bytes // 0), - total_packets: (.total_packets // 0), - bandwidth_kbps: (if .total_packets > 0 then ((.total_bytes * 8) / 1000 / (if .duration > 0 then .duration else 1 end)) else 0 end | floor), - category: (if (.detected_application | test("netflix|youtube|disney|primevideo|twitch|hulu|hbo|vimeo"; "i")) then "video" - elif (.detected_application | test("spotify|apple.*music|deezer|soundcloud|tidal"; "i")) then "audio" - elif (.detected_application | test("zoom|teams|meet|discord|skype|webex"; "i")) then "visio" - else "other" end), - quality: (if .total_packets > 0 then - (if ((.total_bytes * 8) / 1000 / (if .duration > 0 then .duration else 1 end)) < 1000 then "SD" - elif ((.total_bytes * 8) / 1000 / (if .duration > 0 then .duration else 1 end)) < 3000 then "HD" - elif ((.total_bytes * 8) / 1000 / (if .duration > 0 then .duration else 1 end)) < 8000 then "FHD" - else "4K" end) - else "SD" end) - }]' 2>/dev/null) || result="[]" - - echo "$result" + # Extract interface stats from netifyd + echo "$netifyd_data" | jq -c '{ + interfaces: (if .stats then + [.stats | to_entries[] | { + name: .key, + rx_bytes: (.value.ip_bytes // 0), + tx_bytes: (.value.wire_bytes // 0), + rx_packets: (.value.ip // 0), + tx_packets: (.value.raw // 0), + tcp: (.value.tcp // 0), + udp: (.value.udp // 0), + icmp: (.value.icmp // 0) + }] + else [] end), + flows_active: (.flows_active // 0), + flow_count: (.flow_count // 0), + uptime: (.uptime // 0), + agent_version: (.agent_version // 0), + cpu_system: (.cpu_system // 0), + cpu_user: (.cpu_user // 0), + memrss_kb: (.memrss_kb // 0) + }' 2>/dev/null || echo '{"interfaces":[],"flows_active":0,"flow_count":0,"uptime":0,"agent_version":0}' } case "$1" in @@ -100,6 +59,7 @@ case "$1" in { "status": {}, "get_active_streams": {}, + "get_network_stats": {}, "get_stream_history": {"hours": 24}, "get_stats_by_service": {}, "get_stats_by_client": {}, @@ -123,16 +83,17 @@ case "$1" in pgrep netifyd > /dev/null 2>&1 && netifyd_running=1 netifyd_data=$(get_netifyd_data) - active_count=0 + flow_count=0 + netifyd_version="unknown" - if [ "$netifyd_running" = "1" ] && [ -n "$netifyd_data" ]; then - active_count=$(build_active_streams_json "$netifyd_data" | jq 'length' 2>/dev/null || echo 0) + if [ "$netifyd_running" = "1" ] && [ -n "$netifyd_data" ] && [ "$netifyd_data" != "{}" ]; then + flow_count=$(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") fi history_count=0 [ -f "$HISTORY_FILE" ] && history_count=$(jq 'length' "$HISTORY_FILE" 2>/dev/null || echo 0) - # Get settings 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") @@ -140,11 +101,13 @@ case "$1" in { "enabled": $enabled, "module": "media-flow", - "version": "0.5.0", + "version": "0.5.2", "netifyd_running": $netifyd_running, - "active_streams": $active_count, + "netifyd_version": "$netifyd_version", + "active_flows": $flow_count, "history_entries": $history_count, - "refresh_interval": $refresh + "refresh_interval": $refresh, + "note": "netifyd 5.x requires cloud subscription for streaming detection" } EOF ;; @@ -153,13 +116,28 @@ case "$1" in init_storage netifyd_data=$(get_netifyd_data) - streams=$(build_active_streams_json "$netifyd_data") + flow_count=$(echo "$netifyd_data" | jq '.flows_active // .flow_count // 0' 2>/dev/null || echo 0) + # netifyd 5.x doesn't export application detection locally + # Return empty streams with explanation cat <<-EOF - {"streams": $streams} + { + "streams": [], + "note": "Application detection requires netifyd cloud subscription", + "flow_count": $flow_count + } EOF ;; + get_network_stats) + init_storage + + netifyd_data=$(get_netifyd_data) + stats=$(build_network_stats_json "$netifyd_data") + + echo "{\"stats\": $stats}" + ;; + get_stream_history) read -r input hours=$(echo "$input" | jq -r '.hours // 24' 2>/dev/null) @@ -169,7 +147,6 @@ case "$1" in history="[]" if [ -f "$HISTORY_FILE" ]; then - # Get history (cutoff filtering done client-side for simplicity) history=$(jq -c '.' "$HISTORY_FILE" 2>/dev/null || echo "[]") fi @@ -308,7 +285,7 @@ case "$1" in list_alerts) alerts="[]" - # Use jq to build the alerts array from UCI + # Build alerts array from UCI alerts=$(uci show media_flow 2>/dev/null | grep "=alert$" | while read -r line; do section=$(echo "$line" | cut -d. -f2 | cut -d= -f1) svc=$(uci -q get "media_flow.${section}.service")