fix(media-flow): Fix dashboard theming and flow count display
- Add SecuBox dark theme initialization to all views (dashboard, alerts,
clients, services, history)
- Fix flow count detection by using jsonfilter instead of jq (OpenWrt native)
- Prioritize /var/run/netifyd/status.json for ndpid-compat flow data
- Remove filtering expect{} from API.getActiveStreams() RPC declaration
- Update CLAUDE.md with jsonfilter usage guidelines for OpenWrt
The dashboard now correctly displays:
- Total Flows count from nDPId via ndpid-compat
- nDPId/Netifyd status indicators
- SecuBox dark theme with portal header
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
88d60cc7f4
commit
f86b67ce13
18
CLAUDE.md
18
CLAUDE.md
@ -12,6 +12,24 @@
|
|||||||
- `ss` command may not be available - use `netstat` or `/proc/net/tcp` as fallbacks
|
- `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)
|
- `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
|
### Port Detection
|
||||||
When checking if a port is listening, use this order of fallbacks:
|
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)
|
1. `/proc/net/tcp` (always available) - ports are in hex (e.g., 8080 = 1F90)
|
||||||
|
|||||||
@ -11,7 +11,7 @@ var callStatus = rpc.declare({
|
|||||||
var callGetActiveStreams = rpc.declare({
|
var callGetActiveStreams = rpc.declare({
|
||||||
object: 'luci.media-flow',
|
object: 'luci.media-flow',
|
||||||
method: 'get_active_streams',
|
method: 'get_active_streams',
|
||||||
expect: { streams: [] }
|
expect: { }
|
||||||
});
|
});
|
||||||
|
|
||||||
var callGetStreamHistory = rpc.declare({
|
var callGetStreamHistory = rpc.declare({
|
||||||
|
|||||||
@ -7,6 +7,18 @@
|
|||||||
'require secubox-portal/header as SbHeader';
|
'require secubox-portal/header as SbHeader';
|
||||||
|
|
||||||
return L.view.extend({
|
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() {
|
load: function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
API.listAlerts()
|
API.listAlerts()
|
||||||
@ -14,6 +26,9 @@ return L.view.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
render: function(data) {
|
||||||
|
var self = this;
|
||||||
|
this.initTheme();
|
||||||
|
|
||||||
var alerts = data[0] || [];
|
var alerts = data[0] || [];
|
||||||
|
|
||||||
var m = new form.Map('media_flow', null, null);
|
var m = new form.Map('media_flow', null, null);
|
||||||
|
|||||||
@ -6,6 +6,18 @@
|
|||||||
'require secubox-portal/header as SbHeader';
|
'require secubox-portal/header as SbHeader';
|
||||||
|
|
||||||
return L.view.extend({
|
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() {
|
load: function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
API.getStatsByClient()
|
API.getStatsByClient()
|
||||||
@ -13,6 +25,8 @@ return L.view.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
render: function(data) {
|
||||||
|
this.initTheme();
|
||||||
|
|
||||||
var statsByClient = data[0] || {};
|
var statsByClient = data[0] || {};
|
||||||
var clients = statsByClient.clients || {};
|
var clients = statsByClient.clients || {};
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,26 @@ return view.extend({
|
|||||||
title: _('Media Flow Dashboard'),
|
title: _('Media Flow Dashboard'),
|
||||||
pollInterval: 5,
|
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) {
|
formatBytes: function(bytes) {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
var k = 1024;
|
var k = 1024;
|
||||||
@ -28,6 +48,10 @@ return view.extend({
|
|||||||
|
|
||||||
render: function(data) {
|
render: function(data) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
// Initialize SecuBox dark theme
|
||||||
|
this.initTheme();
|
||||||
|
|
||||||
var status = data[0] || {};
|
var status = data[0] || {};
|
||||||
var streamsData = data[1] || {};
|
var streamsData = data[1] || {};
|
||||||
var statsByService = data[2] || {};
|
var statsByService = data[2] || {};
|
||||||
@ -36,7 +60,7 @@ return view.extend({
|
|||||||
var isNdpid = dpiSource === 'ndpid';
|
var isNdpid = dpiSource === 'ndpid';
|
||||||
var isNetifyd = dpiSource === 'netifyd';
|
var isNetifyd = dpiSource === 'netifyd';
|
||||||
var streams = streamsData.streams || [];
|
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
|
// Inject CSS
|
||||||
var css = `
|
var css = `
|
||||||
|
|||||||
@ -6,6 +6,18 @@
|
|||||||
'require secubox-portal/header as SbHeader';
|
'require secubox-portal/header as SbHeader';
|
||||||
|
|
||||||
return L.view.extend({
|
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() {
|
load: function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
API.getStreamHistory(24)
|
API.getStreamHistory(24)
|
||||||
@ -13,6 +25,8 @@ return L.view.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
render: function(data) {
|
||||||
|
this.initTheme();
|
||||||
|
|
||||||
var historyData = data[0] || {};
|
var historyData = data[0] || {};
|
||||||
var history = historyData.history || [];
|
var history = historyData.history || [];
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,18 @@
|
|||||||
'require secubox-portal/header as SbHeader';
|
'require secubox-portal/header as SbHeader';
|
||||||
|
|
||||||
return L.view.extend({
|
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() {
|
load: function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
API.getStatsByService()
|
API.getStatsByService()
|
||||||
@ -13,6 +25,8 @@ return L.view.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
render: function(data) {
|
||||||
|
this.initTheme();
|
||||||
|
|
||||||
var statsByService = data[0] || {};
|
var statsByService = data[0] || {};
|
||||||
var services = statsByService.services || {};
|
var services = statsByService.services || {};
|
||||||
|
|
||||||
|
|||||||
@ -24,8 +24,15 @@ init_storage() {
|
|||||||
|
|
||||||
# Detect available DPI source
|
# Detect available DPI source
|
||||||
get_dpi_source() {
|
get_dpi_source() {
|
||||||
# Prefer nDPId if running and has data
|
# Prefer nDPId if running
|
||||||
if [ -f "$NDPID_FLOWS" ] && pgrep ndpid >/dev/null 2>&1; then
|
# 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"
|
echo "ndpid"
|
||||||
elif [ -f /var/run/netifyd/status.json ] && pgrep netifyd >/dev/null 2>&1; then
|
elif [ -f /var/run/netifyd/status.json ] && pgrep netifyd >/dev/null 2>&1; then
|
||||||
echo "netifyd"
|
echo "netifyd"
|
||||||
@ -124,9 +131,19 @@ case "$1" in
|
|||||||
ndpid_version="unknown"
|
ndpid_version="unknown"
|
||||||
ndpid_flows=0
|
ndpid_flows=0
|
||||||
pgrep ndpid > /dev/null 2>&1 && ndpid_running=1
|
pgrep ndpid > /dev/null 2>&1 && ndpid_running=1
|
||||||
if [ "$ndpid_running" = "1" ] && [ -f "$NDPID_FLOWS" ]; then
|
if [ "$ndpid_running" = "1" ]; 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)
|
||||||
ndpid_version=$(ndpid -v 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
[ -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
|
fi
|
||||||
|
|
||||||
# Check netifyd status
|
# Check netifyd status
|
||||||
@ -137,9 +154,12 @@ case "$1" in
|
|||||||
netifyd_flows=0
|
netifyd_flows=0
|
||||||
netifyd_version="unknown"
|
netifyd_version="unknown"
|
||||||
|
|
||||||
if [ "$netifyd_running" = "1" ] && [ -n "$netifyd_data" ] && [ "$netifyd_data" != "{}" ]; then
|
if [ "$netifyd_running" = "1" ] && [ -f /var/run/netifyd/status.json ] && [ -s /var/run/netifyd/status.json ]; then
|
||||||
netifyd_flows=$(echo "$netifyd_data" | jq '.flows_active // .flow_count // 0' 2>/dev/null || echo 0)
|
netifyd_flows=$(jsonfilter -i /var/run/netifyd/status.json -e '@.flows_active' 2>/dev/null)
|
||||||
netifyd_version=$(echo "$netifyd_data" | jq -r '.agent_version // "unknown"' 2>/dev/null || echo "unknown")
|
[ -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
|
fi
|
||||||
|
|
||||||
# Determine active DPI source
|
# Determine active DPI source
|
||||||
@ -153,11 +173,17 @@ case "$1" in
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
history_count=0
|
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
|
# Check if nDPId cache has active streams
|
||||||
active_streams=0
|
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")
|
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")
|
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
|
if [ -f "$MEDIA_CACHE" ]; then
|
||||||
streams=$(cat "$MEDIA_CACHE" 2>/dev/null || echo "[]")
|
streams=$(cat "$MEDIA_CACHE" 2>/dev/null || echo "[]")
|
||||||
fi
|
fi
|
||||||
if [ -f "$NDPID_FLOWS" ]; then
|
# Get flow count - prefer compat layer status.json
|
||||||
flow_count=$(jq 'length' "$NDPID_FLOWS" 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 '@.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
|
fi
|
||||||
note="Streams detected via nDPId local DPI"
|
note="Streams detected via nDPId local DPI"
|
||||||
elif [ "$dpi_source" = "netifyd" ]; then
|
elif [ "$dpi_source" = "netifyd" ]; then
|
||||||
netifyd_data=$(get_netifyd_data)
|
if [ -f /var/run/netifyd/status.json ] && [ -s /var/run/netifyd/status.json ]; then
|
||||||
flow_count=$(echo "$netifyd_data" | jq '.flows_active // .flow_count // 0' 2>/dev/null || echo 0)
|
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"
|
note="Application detection requires netifyd cloud subscription"
|
||||||
else
|
else
|
||||||
note="No DPI engine available"
|
note="No DPI engine available"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user