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 <noreply@anthropic.com>
This commit is contained in:
parent
17bdd6e80b
commit
c9f719a8de
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user