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:
CyberMind-FR 2026-01-13 07:31:21 +01:00
parent 88d60cc7f4
commit f86b67ce13
8 changed files with 150 additions and 16 deletions

View File

@ -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)

View File

@ -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({

View File

@ -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);

View File

@ -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 || {};

View File

@ -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 = `

View File

@ -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 || [];

View File

@ -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 || {};

View File

@ -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"