feat(kernel): Add complete nftables support + busybox rescue docs

Kernel nftables fix (issue #64):
- Added CONFIG_NF_TABLES_INET, CONFIG_NF_TABLES_IPV4/IPV6
- Added NFT_CT, NFT_LOG, NFT_LIMIT, NFT_REJECT, NFT_COUNTER
- CrowdSec firewall bouncer now works correctly

Build script safety:
- Added deploy_to_device() with safe module extraction
- Prevents /lib symlink overwrite disaster

Documentation:
- FAQ-BUSYBOX-RESCUE.md: Emergency recovery when libc inaccessible
- PROMPT-BUSYBOX-TECHTIP.md: Gemini prompt for visual tech tip
- QUICKSHEET: Added emergency recovery section

Sidebar v2.32.0:
- Per-LED tooltips (Hardware/Services/Security)
- Fixed tooltip positioning for sidebar elements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-05-08 18:27:30 +02:00
parent 81ec2ede51
commit 00766234d2
9 changed files with 708 additions and 42 deletions

View File

@ -4,6 +4,40 @@
---
## 2026-05-08
### Session 128 — LED Tooltips + Kernel nftables Fix
**Sidebar v2.32.0:**
- Per-LED tooltips: each LED row (Hardware/Services/Security) now has its own tooltip
- Hardware tooltip: CPU/MEM/DISK/LOAD histograms with current/average/max values
- Services tooltip: OK/WARN/ERROR/UNKNOWN service counts
- Security tooltip: Active bans and recent alerts from CrowdSec
- Fixed tooltip positioning for sidebar elements (was offset by navbar width)
**SOC Page Fix:**
- Fixed nginx `/soc/` route that was incorrectly proxying to API instead of serving static files
- SOC dashboard HTML now loads correctly
**Kernel nftables Issue (GitHub #64):**
- Discovered kernel 6.6.137 missing critical nftables options:
- `CONFIG_NF_TABLES_INET` - inet family support
- `CONFIG_NF_TABLES_IPV4` - IPv4 rules
- `CONFIG_NFT_CT`, `CONFIG_NFT_LOG`, `CONFIG_NFT_REJECT`, etc.
- CrowdSec bouncer in restart loop due to `Operation not supported` errors
- Updated `board/mochabin/kernel/config-6.12-openwrt-merged.fragment` with complete nftables config
- Created issue #64 with bouncer health alert requirements (CSPN critical)
**Files Updated:**
- `www/shared/sidebar.js` — v2.32.0 with per-LED tooltips
- `www/shared/hybrid-skin.css` — Service/Security tooltip CSS styles
- `board/mochabin/kernel/config-6.12-openwrt-merged.fragment` — Full nftables support
---
### Session 127 — Smart Strip + LED Pulsing + Round UI Virtual
*(earlier today - see v2.25.0 to v2.31.0 changes)*
---
### Session 126 — Hybrid Skin License & Centralized Injection
**License Headers Added:**

View File

@ -117,6 +117,72 @@ Each module card contains:
---
## 🚨 Emergency Recovery — Busybox Anti-Crash
### Problem: `/lib` symlink broken (libc inaccessible)
On modern Debian, `/lib` is a symlink to `/usr/lib`. If a tarball extraction replaces this symlink with a directory, ALL external commands fail:
```
-bash: /usr/bin/ls: cannot execute: required file not found
```
### Solution: Busybox (statically linked)
Busybox is often compiled statically and doesn't need libc:
```bash
# Check if busybox exists
echo /bin/busybox
echo /usr/bin/busybox
# If found, use it to repair:
/bin/busybox rm -rf /lib
/bin/busybox ln -s usr/lib /lib
# Verify
/bin/busybox ls -la /lib
```
### Prevention: Safe module tarball extraction
```bash
# WRONG - can overwrite /lib symlink
cd / && tar xzf modules.tar.gz
# RIGHT - extract to specific path
tar xzf modules.tar.gz -C /lib/modules/ --strip-components=2
```
### Shell built-ins when nothing works
If no busybox, use bash built-ins to diagnose:
```bash
# List files (glob expansion)
echo /lib/*
echo /usr/lib/aarch64-linux-gnu/libc*
# Read file content
while IFS= read -r line; do echo "$line"; done < /etc/os-release
# Check if path exists
[[ -e /lib/aarch64-linux-gnu ]] && echo "exists" || echo "missing"
```
### Last resort: Rescue boot
Boot from USB/SD rescue media:
```bash
mount /dev/mmcblk0p2 /mnt
rm -rf /mnt/lib
ln -s usr/lib /mnt/lib
umount /mnt
reboot
```
---
## Brand Footer
*CyberMind · Gondwana · Notre-Dame-du-Cruet · Savoie*
@ -124,3 +190,4 @@ Each module card contains:
---
*Reference extracted: 2026-04-08*
*Emergency recovery added: 2026-05-08*

View File

@ -1,5 +1,33 @@
# WIP — Work In Progress
*Mis à jour : 2026-05-08 (Session 127)*
*Mis à jour : 2026-05-08 (Session 128)*
---
## 🔄 Session 128: LED Tooltips + Kernel NFTables Fix
### Sidebar v2.32.0 — IN PROGRESS
- [x] Per-LED tooltips (separate for Hardware/Services/Security)
- [x] Fixed tooltip positioning for sidebar elements
- [x] Service layer tooltip with OK/WARN/ERROR/UNKNOWN counts
- [x] Security layer tooltip with bans/alerts stats
- [x] Hardware layer with CPU/MEM/DISK/LOAD histograms
### SOC Page Fix — COMPLETE
- [x] Fixed nginx route for /soc/ (was proxying to API, now serves static files)
- [x] SOC page HTML now loads correctly
### Kernel nftables Issue #64 — CRITICAL
- [x] Identified missing kernel options: `CONFIG_NF_TABLES_INET`, `CONFIG_NF_TABLES_IPV4`
- [x] Created GitHub issue #64 with full config requirements
- [x] Updated `board/mochabin/kernel/config-6.12-openwrt-merged.fragment` with complete nftables config
- [ ] Rebuild kernel with new config
- [ ] Deploy new kernel to MOCHAbin
- [ ] Add nftables modules to initramfs for early boot
### CrowdSec Bouncer Alert — TODO (ref issue #64)
- [ ] Add pre-flight nftables check to bouncer startup
- [ ] Alert SOC dashboard if firewall offline
- [ ] Set LED3 (Security) to RED if nftables fails
---

View File

@ -143,13 +143,66 @@ CONFIG_IP_ADVANCED_ROUTER=y
CONFIG_IP_MULTIPLE_TABLES=y
CONFIG_IP_ROUTE_MULTIPATH=y
CONFIG_IPV6=y
# NETFILTER / NFTABLES - Complete firewall support (ref: GitHub issue #64)
CONFIG_NETFILTER=y
CONFIG_NF_CONNTRACK=y
CONFIG_NETFILTER_ADVANCED=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_NETFILTER_NETLINK_ACCT=y
CONFIG_NETFILTER_NETLINK_QUEUE=y
CONFIG_NETFILTER_NETLINK_LOG=y
CONFIG_NF_CONNTRACK=y
CONFIG_NF_CONNTRACK_MARK=y
CONFIG_NF_CONNTRACK_ZONES=y
CONFIG_NF_CONNTRACK_EVENTS=y
CONFIG_NF_CONNTRACK_TIMEOUT=y
CONFIG_NF_CONNTRACK_TIMESTAMP=y
CONFIG_NF_CT_PROTO_DCCP=y
CONFIG_NF_CT_PROTO_SCTP=y
CONFIG_NF_CT_PROTO_UDPLITE=y
CONFIG_NF_CT_NETLINK=y
CONFIG_NF_NAT=y
CONFIG_NF_NAT_MASQUERADE=y
CONFIG_NF_TABLES=y
CONFIG_NFT_NAT=y
CONFIG_NF_TABLES_INET=y
CONFIG_NF_TABLES_IPV4=y
CONFIG_NF_TABLES_IPV6=y
CONFIG_NF_TABLES_NETDEV=y
CONFIG_NFT_NUMGEN=y
CONFIG_NFT_CT=y
CONFIG_NFT_COUNTER=y
CONFIG_NFT_CONNLIMIT=y
CONFIG_NFT_LOG=y
CONFIG_NFT_LIMIT=y
CONFIG_NFT_MASQ=y
CONFIG_NFT_REDIR=y
CONFIG_NFT_NAT=y
CONFIG_NFT_REJECT=y
CONFIG_NFT_HASH=y
CONFIG_NFT_QUOTA=y
CONFIG_NFT_SOCKET=y
CONFIG_NFT_TPROXY=y
CONFIG_NFT_FIB=y
CONFIG_NFT_FIB_INET=y
CONFIG_NFT_FIB_IPV4=y
CONFIG_NFT_FIB_IPV6=y
CONFIG_NFT_CHAIN_NAT=y
CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y
CONFIG_NETFILTER_XT_MATCH_STATE=y
CONFIG_NETFILTER_XT_MATCH_COMMENT=y
CONFIG_NETFILTER_XT_MATCH_LIMIT=y
CONFIG_NETFILTER_XT_MATCH_MULTIPORT=y
CONFIG_NETFILTER_XT_TARGET_LOG=y
CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y
CONFIG_NF_DEFRAG_IPV4=y
CONFIG_NF_DEFRAG_IPV6=y
CONFIG_IP_NF_IPTABLES=y
CONFIG_IP_NF_FILTER=y
CONFIG_IP_NF_NAT=y
CONFIG_IP_NF_TARGET_MASQUERADE=y
CONFIG_IP6_NF_IPTABLES=y
CONFIG_IP6_NF_FILTER=y
CONFIG_IP6_NF_NAT=y
CONFIG_IP6_NF_TARGET_MASQUERADE=y
# VLAN
CONFIG_VLAN_8021Q=y

150
docs/FAQ-BUSYBOX-RESCUE.md Normal file
View File

@ -0,0 +1,150 @@
# FAQ: Busybox Emergency Recovery
## The Problem
**Symptom:** All commands fail with "cannot execute: required file not found"
```bash
root@host:~# ls
-bash: /usr/bin/ls: cannot execute: required file not found
root@host:~# ln -s foo bar
-bash: /usr/bin/ln: cannot execute: required file not found
```
**Cause:** The `/lib` symlink (pointing to `/usr/lib`) was replaced by a directory, breaking access to `libc.so` and the dynamic linker.
**Common trigger:** Extracting a tarball containing `lib/` at root level:
```bash
# DANGEROUS - overwrites /lib symlink!
cd / && tar xzf modules.tar.gz
```
---
## The Solution: Busybox
Busybox is typically **statically linked** - it doesn't need libc to run.
### Step 1: Check if busybox exists
```bash
echo /bin/busybox
echo /usr/bin/busybox
```
### Step 2: Use busybox to fix the symlink
```bash
# Remove broken /lib directory
/bin/busybox rm -rf /lib
# Recreate the symlink
/bin/busybox ln -s usr/lib /lib
# Verify
/bin/busybox ls -la /lib
```
### Step 3: Test recovery
```bash
ls -la /lib/aarch64-linux-gnu/libc.so*
cat /etc/os-release
uname -r
```
---
## Why This Works
| Component | Location | Status when /lib broken |
|-----------|----------|------------------------|
| libc.so | `/usr/lib/aarch64-linux-gnu/` | Exists but inaccessible |
| ld-linux | `/usr/lib/aarch64-linux-gnu/` | Exists but inaccessible |
| /lib | Should be symlink to `usr/lib` | **Broken** - is a directory |
| busybox | `/bin/busybox` | **Works** - statically linked |
Busybox contains 300+ commands in a single static binary. When libc is inaccessible, it's your lifeline.
---
## Prevention
### Safe tarball extraction for kernel modules:
```bash
# WRONG
cd / && tar xzf modules.tar.gz
# RIGHT - extract only the modules subdirectory
tar xzf modules.tar.gz -C /lib/modules/ --strip-components=2
# OR create tarball with correct structure
cd /build/output/modules/lib/modules
tar czf modules-6.6.137.tar.gz 6.6.137
# Then on target:
cd /lib/modules && tar xzf /tmp/modules-6.6.137.tar.gz
```
### Check before extracting:
```bash
# Always check tarball contents first!
tar tzf modules.tar.gz | head -20
# Look for dangerous top-level paths:
# lib/ <- DANGER if extracted at /
# usr/ <- DANGER
# bin/ <- DANGER
```
---
## Shell Built-ins When Nothing Works
If no busybox available, bash built-ins still work:
```bash
# List files (glob expansion)
echo /lib/*
echo /usr/lib/aarch64-linux-gnu/libc*
# Read file content
while IFS= read -r line; do echo "$line"; done < /etc/os-release
# Check if path exists
[[ -d /lib/aarch64-linux-gnu ]] && echo "exists" || echo "missing"
# Write to file
echo "test" > /tmp/test
```
---
## Last Resort: Rescue Boot
If no busybox and shell built-ins can't help:
1. Boot from USB/SD rescue media
2. Mount the broken rootfs:
```bash
mount /dev/mmcblk0p2 /mnt
```
3. Fix the symlink:
```bash
rm -rf /mnt/lib
ln -s usr/lib /mnt/lib
```
4. Reboot normally
---
## Key Takeaways
1. **Always have busybox installed** (statically linked)
2. **Never extract tarballs blindly at /**
3. **Check tarball contents before extracting**
4. **Know your shell built-ins** (echo, read, [[ ]])
5. **Keep rescue media ready** for emergencies
---
*SecuBox-Deb Emergency Recovery Guide*
*CyberMind — https://cybermind.fr*
*Author: Gerald Kerma <gandalf@gk2.net>*
*Incident: 2026-05-08*

View File

@ -0,0 +1,105 @@
# Gemini Prompt: Busybox Emergency Recovery Tech Tip
## Prompt for Quick Visual Sketch
```
Create a quick tech tip visual sketch/infographic about "Busybox Emergency Recovery" for Linux sysadmins.
SCENARIO:
- All commands fail: "cannot execute: required file not found"
- Cause: /lib symlink to /usr/lib was accidentally replaced by a directory
- libc.so exists but is inaccessible
- System appears completely broken
THE HERO: BUSYBOX
- Busybox is STATICALLY LINKED - doesn't need libc!
- Contains 300+ commands in one binary
- Your lifeline when dynamic linking breaks
RECOVERY STEPS (3 commands):
1. /bin/busybox rm -rf /lib
2. /bin/busybox ln -s usr/lib /lib
3. /bin/busybox ls -la /lib (verify)
VISUAL ELEMENTS:
- Show broken chain (dynamic linking) vs solid block (static busybox)
- Traffic light: RED (broken) → GREEN (fixed)
- Terminal screenshots of the error and fix
- Busybox Swiss Army knife icon
PREVENTION TIP:
"Never extract tarballs blindly at root!"
tar tzf file.tar.gz | head # ALWAYS check first!
STYLE:
- Dark terminal aesthetic (green on black)
- Minimalist, quick-reference format
- One page maximum
- Logo: CyberMind SecuBox
TAGLINE:
"When libc fails, busybox prevails"
```
---
## Alternative: ASCII Art Version
```
┌─────────────────────────────────────────────────────────────┐
│ 🚨 BUSYBOX EMERGENCY RECOVERY │
├─────────────────────────────────────────────────────────────┤
│ │
│ SYMPTOM: │
│ $ ls │
│ -bash: /usr/bin/ls: cannot execute: required file not found│
│ │
│ CAUSE: /lib symlink broken → libc inaccessible │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ /lib │ ──X─│ libc.so │ Dynamic linking BROKEN │
│ │ (dir) │ │ │ │
│ └─────────┘ └─────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ BUSYBOX │ Static binary = WORKS! │
│ │ [===============] │ │
│ └─────────────────────────┘ │
│ │
│ FIX (3 commands): │
│ ┌─────────────────────────────────────────────────┐ │
│ │ /bin/busybox rm -rf /lib │ │
│ │ /bin/busybox ln -s usr/lib /lib │ │
│ │ /bin/busybox ls -la /lib # verify │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ✓ System restored! │
│ │
├─────────────────────────────────────────────────────────────┤
│ 💡 PREVENTION: tar tzf file.tar.gz | head # check first! │
├─────────────────────────────────────────────────────────────┤
│ "When libc fails, busybox prevails" │
│ — CyberMind SecuBox Tech Tips │
└─────────────────────────────────────────────────────────────┘
```
---
## Short Version (Tweet/Toot)
```
🚨 Linux Emergency: All commands fail with "cannot execute"?
Your /lib symlink is broken! Fix with static busybox:
/bin/busybox rm -rf /lib
/bin/busybox ln -s usr/lib /lib
"When libc fails, busybox prevails" 🔧
#Linux #SysAdmin #TechTip
```
---
*CyberMind SecuBox — 2026-05-08*

View File

@ -65,6 +65,27 @@ install_output() {
ls -la "$OUTPUT_DIR/"
}
deploy_to_device() {
local HOST="${1:-root@192.168.1.200}"
log "Deploying to $HOST..."
# Deploy kernel and DTB
scp "$OUTPUT_DIR/Image-${KERNEL_VERSION}-secubox" "$HOST:/boot/"
scp "$OUTPUT_DIR/armada-7040-mochabin.dtb" "$HOST:/boot/"
# SAFE module deployment - don't overwrite /lib symlink!
# Create tarball with correct structure for /lib/modules/ extraction
log "Creating safe modules tarball..."
cd "$OUTPUT_DIR/modules/lib/modules"
tar czf "/tmp/modules-${KERNEL_VERSION}.tar.gz" "${KERNEL_VERSION}"
# Deploy and extract safely
scp "/tmp/modules-${KERNEL_VERSION}.tar.gz" "$HOST:/tmp/"
ssh "$HOST" "mkdir -p /lib/modules && cd /lib/modules && tar xzf /tmp/modules-${KERNEL_VERSION}.tar.gz && depmod -a ${KERNEL_VERSION} || true"
log "Deployed! Add boot entry and reboot."
}
case "${1:-build}" in
build) check_deps; download_kernel; apply_patches; configure_kernel; build_kernel; install_output ;;
deps) check_deps ;;

View File

@ -1250,20 +1250,39 @@ body.hybrid-skin .map {
transform: scale(1.1);
}
/* Header LEDs (sidebar header - next to SECUBOX logo) */
.header-hw-leds {
/* Header LEDs with metrics (sidebar header - next to SECUBOX logo) */
.header-leds-metrics {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
padding: 4px 6px;
gap: 2px;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.4);
border-radius: 5px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
margin-left: auto;
cursor: pointer;
}
.led-metric-row {
display: flex;
align-items: center;
gap: 6px;
}
.led-metric-val {
font-size: 9px;
font-weight: 600;
font-family: var(--font-mono, 'JetBrains Mono', monospace);
color: var(--text-light, #e8e6d9);
min-width: 28px;
text-align: right;
}
.led-metric-val.ok { color: #00dd44; }
.led-metric-val.warn { color: #ffb347; }
.led-metric-val.error { color: #ff4466; }
.led-metric-val.info { color: #4488ff; }
.sidebar-header {
display: flex !important;
align-items: center !important;
@ -1276,10 +1295,24 @@ body.hybrid-skin .map {
gap: 8px !important;
}
.header-hw-leds:hover .hw-led-v {
.header-leds-metrics:hover .hw-led-v {
transform: scale(1.15);
}
/* Legacy - kept for compatibility */
.header-hw-leds {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
padding: 4px 6px;
background: rgba(0, 0, 0, 0.4);
border-radius: 5px;
border: 1px solid rgba(255, 255, 255, 0.1);
margin-left: auto;
cursor: pointer;
}
/* Hardware LED Tooltip */
.hw-led-tooltip {
display: none;
@ -1397,6 +1430,74 @@ body.hybrid-skin .map {
margin-bottom: 8px;
}
.hw-tooltip-footer {
font-size: 9px;
color: var(--muted-dark, #6b6b7a);
text-align: center;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* Services layer tooltip */
.svc-metrics {
display: flex;
flex-direction: row;
justify-content: space-around;
gap: 8px;
padding: 10px 0;
}
.svc-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.svc-count {
font-size: 18px;
font-weight: 700;
font-family: var(--font-mono, 'JetBrains Mono', monospace);
}
.svc-label {
font-size: 8px;
color: var(--muted-dark, #6b6b7a);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Security layer tooltip */
.sec-metrics {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 0;
}
.sec-stat {
display: flex;
align-items: center;
gap: 8px;
}
.sec-icon {
font-size: 16px;
}
.sec-count {
font-size: 20px;
font-weight: 700;
font-family: var(--font-mono, 'JetBrains Mono', monospace);
color: var(--text-light, #e8e6d9);
min-width: 40px;
}
.sec-label {
font-size: 11px;
color: var(--muted-dark, #6b6b7a);
}
/* Mini LED Strip (legacy - kept for compatibility) */
.mini-led-strip {
display: flex;

View File

@ -21,7 +21,7 @@
(function() {
const MENU_API = '/api/v1/hub/public/menu';
const BATCH_HEALTH_API = '/api/v1/hub/public/health-batch';
const VERSION = 'v2.30.0';
const VERSION = 'v2.32.0';
// Round UI Module Colors (6-module system)
const MODULE_COLORS = {
@ -360,12 +360,13 @@
}
// Hardware LED tooltip - shows detailed health info with multi-layer histogram
function showHwLedTooltip(el) {
// layer: 1=Hardware, 2=Services, 3=Security
function showHwLedTooltip(el, layer) {
var tooltip = document.getElementById('hw-led-tooltip');
if (!tooltip) return;
var healthLabels = { ok: 'NOMINAL', warn: 'WARNING', error: 'CRITICAL' };
var healthColors = { ok: '#00dd44', warn: '#ffb347', error: '#ff4466' };
var healthColors = { ok: '#00dd44', warn: '#ffb347', error: '#ff4466', info: '#4488ff' };
// Calculate stats from history (top/max/medium)
function getStats(arr) {
@ -377,11 +378,6 @@
return { cur: cur, max: max, avg: avg };
}
var cpuStats = getStats(stripMetricsHistory.cpu);
var memStats = getStats(stripMetricsHistory.mem);
var diskStats = getStats(stripMetricsHistory.disk);
var loadStats = getStats(stripMetricsHistory.load);
// Multi-layer histogram bars (top=max, medium=avg, bottom=current)
function makeHistoBar(stats, color) {
var maxPct = Math.min(100, stats.max);
@ -394,29 +390,84 @@
'</div>';
}
// Build tooltip content with multi-layer histograms
var content = '<div class="hw-tooltip-header" style="color:' + healthColors[systemHealthLevel] + '">' +
'<span class="hw-tooltip-icon">●</span> ' + healthLabels[systemHealthLevel] +
'</div>' +
'<div class="hw-tooltip-metrics">' +
'<div class="hw-metric-row"><span class="hw-metric-label">CPU</span>' + makeHistoBar(cpuStats, '#C04E24') + '<span class="hw-metric-vals">' + cpuStats.cur + ' / ' + cpuStats.max + '</span></div>' +
'<div class="hw-metric-row"><span class="hw-metric-label">MEM</span>' + makeHistoBar(memStats, '#9A6010') + '<span class="hw-metric-vals">' + memStats.cur + ' / ' + memStats.max + '</span></div>' +
'<div class="hw-metric-row"><span class="hw-metric-label">DISK</span>' + makeHistoBar(diskStats, '#803018') + '<span class="hw-metric-vals">' + diskStats.cur + ' / ' + diskStats.max + '</span></div>' +
'<div class="hw-metric-row"><span class="hw-metric-label">LOAD</span>' + makeHistoBar({cur: loadStats.cur * 25, max: loadStats.max * 25, avg: loadStats.avg * 25}, '#3D35A0') + '<span class="hw-metric-vals">' + (loadStats.cur).toFixed(1) + ' / ' + (loadStats.max).toFixed(1) + '</span></div>' +
'</div>' +
'<div class="hw-tooltip-legend"><span>■ Current</span><span style="opacity:0.5">■ Average</span><span style="opacity:0.3">■ Max</span></div>' +
'<div class="hw-tooltip-leds">' +
'<div><span style="color:#00dd44">●</span> System</div>' +
'<div><span style="color:' + (systemHealthLevel === 'ok' ? '#00dd44' : '#ffb347') + '">●</span> Services</div>' +
'<div><span style="color:' + healthColors[systemHealthLevel] + '">●</span> Security</div>' +
'</div>';
var content = '';
var layerTitles = { 1: 'HARDWARE', 2: 'SERVICES', 3: 'SECURITY' };
var layerIcons = { 1: '⚙️', 2: '🔧', 3: '🛡️' };
if (layer === 1) {
// Hardware layer - CPU, MEM, DISK, LOAD
var cpuStats = getStats(stripMetricsHistory.cpu);
var memStats = getStats(stripMetricsHistory.mem);
var diskStats = getStats(stripMetricsHistory.disk);
var loadStats = getStats(stripMetricsHistory.load);
var hwMax = Math.max(cpuStats.cur, memStats.cur);
var hwLevel = hwMax > 80 ? 'error' : hwMax > 50 ? 'warn' : 'ok';
content = '<div class="hw-tooltip-header" style="color:' + healthColors[hwLevel] + '">' +
'<span class="hw-tooltip-icon">' + layerIcons[1] + '</span> ' + layerTitles[1] + ' — ' + healthLabels[hwLevel] +
'</div>' +
'<div class="hw-tooltip-metrics">' +
'<div class="hw-metric-row"><span class="hw-metric-label">CPU</span>' + makeHistoBar(cpuStats, '#C04E24') + '<span class="hw-metric-vals">' + cpuStats.cur + '% / ' + cpuStats.max + '%</span></div>' +
'<div class="hw-metric-row"><span class="hw-metric-label">MEM</span>' + makeHistoBar(memStats, '#9A6010') + '<span class="hw-metric-vals">' + memStats.cur + '% / ' + memStats.max + '%</span></div>' +
'<div class="hw-metric-row"><span class="hw-metric-label">DISK</span>' + makeHistoBar(diskStats, '#803018') + '<span class="hw-metric-vals">' + diskStats.cur + '% / ' + diskStats.max + '%</span></div>' +
'<div class="hw-metric-row"><span class="hw-metric-label">LOAD</span>' + makeHistoBar({cur: loadStats.cur * 25, max: loadStats.max * 25, avg: loadStats.avg * 25}, '#3D35A0') + '<span class="hw-metric-vals">' + (loadStats.cur).toFixed(1) + ' / ' + (loadStats.max).toFixed(1) + '</span></div>' +
'</div>' +
'<div class="hw-tooltip-legend"><span>■ Current</span><span style="opacity:0.5">■ Average</span><span style="opacity:0.3">■ Max</span></div>';
} else if (layer === 2) {
// Services layer - count ok/warn/error/total
var counts = { ok: 0, warn: 0, error: 0, unknown: 0 };
Object.keys(healthCache).forEach(function(mod) {
var st = healthCache[mod] && healthCache[mod].status;
if (st === 'ok') counts.ok++;
else if (st === 'warn') counts.warn++;
else if (st === 'error') counts.error++;
else counts.unknown++;
});
var total = counts.ok + counts.warn + counts.error + counts.unknown;
var svcLevel = counts.error > 0 ? 'error' : counts.warn > 0 ? 'warn' : 'ok';
content = '<div class="hw-tooltip-header" style="color:' + healthColors[svcLevel] + '">' +
'<span class="hw-tooltip-icon">' + layerIcons[2] + '</span> ' + layerTitles[2] + ' — ' + healthLabels[svcLevel] +
'</div>' +
'<div class="hw-tooltip-metrics svc-metrics">' +
'<div class="svc-stat"><span class="svc-count" style="color:#00dd44">' + counts.ok + '</span><span class="svc-label">OK</span></div>' +
'<div class="svc-stat"><span class="svc-count" style="color:#ffb347">' + counts.warn + '</span><span class="svc-label">WARN</span></div>' +
'<div class="svc-stat"><span class="svc-count" style="color:#ff4466">' + counts.error + '</span><span class="svc-label">ERROR</span></div>' +
'<div class="svc-stat"><span class="svc-count" style="color:#666">' + counts.unknown + '</span><span class="svc-label">UNKNOWN</span></div>' +
'</div>' +
'<div class="hw-tooltip-footer">' + counts.ok + ' / ' + total + ' services healthy</div>';
} else if (layer === 3) {
// Security layer - bans, alerts, crowdsec status
var bans = currentStripMetrics.bans || 0;
var alerts = currentStripMetrics.alerts || 0;
var secLevel = bans > 50 ? 'info' : bans > 20 ? 'warn' : alerts > 0 ? 'warn' : 'ok';
var secLabel = bans > 50 ? 'ACTIVE MITIGATION' : healthLabels[secLevel === 'info' ? 'ok' : secLevel];
content = '<div class="hw-tooltip-header" style="color:' + healthColors[secLevel] + '">' +
'<span class="hw-tooltip-icon">' + layerIcons[3] + '</span> ' + layerTitles[3] + ' — ' + secLabel +
'</div>' +
'<div class="hw-tooltip-metrics sec-metrics">' +
'<div class="sec-stat"><span class="sec-icon">🚫</span><span class="sec-count">' + bans + '</span><span class="sec-label">Active Bans</span></div>' +
'<div class="sec-stat"><span class="sec-icon">⚠️</span><span class="sec-count">' + alerts + '</span><span class="sec-label">Recent Alerts</span></div>' +
'</div>' +
'<div class="hw-tooltip-footer">CrowdSec ' + (bans > 0 ? 'actively blocking threats' : 'monitoring') + '</div>';
}
tooltip.innerHTML = content;
// Position tooltip
// Position tooltip - check if inside sidebar (left < 220px)
var rect = el.getBoundingClientRect();
tooltip.style.left = rect.left + 'px';
tooltip.style.top = (rect.bottom + 8) + 'px';
var tooltipLeft = rect.left;
// If element is in sidebar (left < 220), position tooltip to the right of it
if (rect.left < 220) {
tooltipLeft = rect.right + 10;
}
tooltip.style.left = tooltipLeft + 'px';
tooltip.style.top = rect.top + 'px';
tooltip.style.display = 'block';
}
@ -662,8 +713,60 @@
healthEl.innerHTML = emoji + ' ' + ok + '/' + total;
healthEl.className = 'status-value ' + (err > 0 ? 'error' : warn > 0 ? 'warn' : 'ok');
healthEl.title = ok + ' OK, ' + warn + ' warnings, ' + err + ' errors';
// Update LED metrics in sidebar header (sync with health bumper)
var lmHw = document.getElementById('lm-hw');
var lmSvc = document.getElementById('lm-svc');
var lmSec = document.getElementById('lm-sec');
// LED1 (Hardware): CPU%
if (lmHw) {
var hwMax = Math.max(currentStripMetrics.cpu || 0, currentStripMetrics.mem || 0);
lmHw.textContent = hwMax + '%';
lmHw.className = 'led-metric-val ' + (hwMax > 80 ? 'error' : hwMax > 50 ? 'warn' : 'ok');
}
// LED2 (Services): ok/total
if (lmSvc) {
lmSvc.textContent = ok + '/' + total;
lmSvc.className = 'led-metric-val ' + (err > 0 ? 'error' : warn > 0 ? 'warn' : 'ok');
}
// LED3 (Security): fetch bans from CrowdSec
if (lmSec) {
// Will be updated by separate security fetch
lmSec.textContent = '...';
}
}
}
// Fetch security metrics (CrowdSec bans)
try {
var secRes = await fetch('/api/v1/crowdsec/stats', { headers: headers });
if (secRes.ok) {
var secData = await secRes.json();
var lmSec = document.getElementById('lm-sec');
if (lmSec) {
var bans = secData.active_bans || secData.decisions || 0;
var alerts = secData.alerts_today || 0;
lmSec.textContent = bans + 'B';
lmSec.title = bans + ' bans, ' + alerts + ' alerts today';
// Blue if actively blocking, else based on alert level
if (bans > 20) {
lmSec.className = 'led-metric-val info'; // Blue - active mitigation
} else if (alerts > 50) {
lmSec.className = 'led-metric-val error';
} else if (alerts > 10) {
lmSec.className = 'led-metric-val warn';
} else {
lmSec.className = 'led-metric-val ok';
}
}
}
} catch (e) {
var lmSec = document.getElementById('lm-sec');
if (lmSec) lmSec.textContent = '?';
}
} catch (e) {
console.log('[Sidebar] Status bar data error:', e);
}
@ -1295,7 +1398,11 @@
hideUnknownEnabled = getHideUnknownPref();
sidebar.innerHTML = '<div class="sidebar-header"><a href="/"><span class="logo-icon">🔒</span><div><span class="logo">SECUBOX</span><span class="logo-version">🚀 ' + VERSION + '</span></div></a>' +
'<div class="header-hw-leds" id="header-hw-leds"><div class="hw-led-v" id="hw-led-3"></div><div class="hw-led-v" id="hw-led-2"></div><div class="hw-led-v" id="hw-led-1"></div></div></div>' +
'<div class="header-leds-metrics" id="header-leds-metrics">' +
'<div class="led-metric-row" onmouseenter="SecuBoxSidebar.showHwLedTooltip(this,3)" onmouseleave="SecuBoxSidebar.hideHwLedTooltip()"><div class="hw-led-v" id="hw-led-3"></div><span class="led-metric-val" id="lm-sec">--</span></div>' +
'<div class="led-metric-row" onmouseenter="SecuBoxSidebar.showHwLedTooltip(this,2)" onmouseleave="SecuBoxSidebar.hideHwLedTooltip()"><div class="hw-led-v" id="hw-led-2"></div><span class="led-metric-val" id="lm-svc">--</span></div>' +
'<div class="led-metric-row" onmouseenter="SecuBoxSidebar.showHwLedTooltip(this,1)" onmouseleave="SecuBoxSidebar.hideHwLedTooltip()"><div class="hw-led-v" id="hw-led-1"></div><span class="led-metric-val" id="lm-hw">--</span></div>' +
'</div></div>' +
'<div class="sidebar-nav"><div class="nav-section"><div class="nav-section-title"><span>Loading...</span></div></div></div>';
try {
@ -1368,10 +1475,10 @@
});
sidebar.innerHTML = '<div class="sidebar-header"><a href="/"><span class="logo-icon">🔒</span><div><span class="logo">SECUBOX</span><span class="logo-version">🚀 ' + VERSION + '</span></div></a>' +
'<div class="header-hw-leds" id="header-hw-leds" onmouseenter="SecuBoxSidebar.showHwLedTooltip(this)" onmouseleave="SecuBoxSidebar.hideHwLedTooltip()">' +
'<div class="hw-led-v" id="hw-led-3" title="Security"></div>' +
'<div class="hw-led-v" id="hw-led-2" title="Services"></div>' +
'<div class="hw-led-v" id="hw-led-1" title="Hardware"></div>' +
'<div class="header-leds-metrics" id="header-leds-metrics">' +
'<div class="led-metric-row" onmouseenter="SecuBoxSidebar.showHwLedTooltip(this,3)" onmouseleave="SecuBoxSidebar.hideHwLedTooltip()"><div class="hw-led-v" id="hw-led-3"></div><span class="led-metric-val" id="lm-sec">--</span></div>' +
'<div class="led-metric-row" onmouseenter="SecuBoxSidebar.showHwLedTooltip(this,2)" onmouseleave="SecuBoxSidebar.hideHwLedTooltip()"><div class="hw-led-v" id="hw-led-2"></div><span class="led-metric-val" id="lm-svc">--</span></div>' +
'<div class="led-metric-row" onmouseenter="SecuBoxSidebar.showHwLedTooltip(this,1)" onmouseleave="SecuBoxSidebar.hideHwLedTooltip()"><div class="hw-led-v" id="hw-led-1"></div><span class="led-metric-val" id="lm-hw">--</span></div>' +
'</div></div>' +
'<div class="sidebar-nav">' + menuHTML + '</div>' +
'<div class="sidebar-footer">' +