diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 0cb988df..737e2a79 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -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:** diff --git a/.claude/QUICKSHEET-REFERENCE.md b/.claude/QUICKSHEET-REFERENCE.md index 9cea3a66..4e4e49d2 100644 --- a/.claude/QUICKSHEET-REFERENCE.md +++ b/.claude/QUICKSHEET-REFERENCE.md @@ -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* diff --git a/.claude/WIP.md b/.claude/WIP.md index 6f0d33ee..69080b0b 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -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 --- diff --git a/board/mochabin/kernel/config-6.12-openwrt-merged.fragment b/board/mochabin/kernel/config-6.12-openwrt-merged.fragment index 8982d081..c4ed6bc7 100644 --- a/board/mochabin/kernel/config-6.12-openwrt-merged.fragment +++ b/board/mochabin/kernel/config-6.12-openwrt-merged.fragment @@ -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 diff --git a/docs/FAQ-BUSYBOX-RESCUE.md b/docs/FAQ-BUSYBOX-RESCUE.md new file mode 100644 index 00000000..f6e9d6d8 --- /dev/null +++ b/docs/FAQ-BUSYBOX-RESCUE.md @@ -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 * +*Incident: 2026-05-08* diff --git a/docs/PROMPT-BUSYBOX-TECHTIP.md b/docs/PROMPT-BUSYBOX-TECHTIP.md new file mode 100644 index 00000000..559326a9 --- /dev/null +++ b/docs/PROMPT-BUSYBOX-TECHTIP.md @@ -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* diff --git a/kernel-build/build-kernel.sh b/kernel-build/build-kernel.sh index 3aa4f5e8..9f5cf4f6 100755 --- a/kernel-build/build-kernel.sh +++ b/kernel-build/build-kernel.sh @@ -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 ;; diff --git a/packages/secubox-hub/www/shared/hybrid-skin.css b/packages/secubox-hub/www/shared/hybrid-skin.css index a9931587..35070e1c 100644 --- a/packages/secubox-hub/www/shared/hybrid-skin.css +++ b/packages/secubox-hub/www/shared/hybrid-skin.css @@ -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; diff --git a/packages/secubox-hub/www/shared/sidebar.js b/packages/secubox-hub/www/shared/sidebar.js index ca63f8a0..2bc76ed5 100644 --- a/packages/secubox-hub/www/shared/sidebar.js +++ b/packages/secubox-hub/www/shared/sidebar.js @@ -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 @@ ''; } - // Build tooltip content with multi-layer histograms - var content = '
' + - ' ' + healthLabels[systemHealthLevel] + - '
' + - '
' + - '
CPU' + makeHistoBar(cpuStats, '#C04E24') + '' + cpuStats.cur + ' / ' + cpuStats.max + '
' + - '
MEM' + makeHistoBar(memStats, '#9A6010') + '' + memStats.cur + ' / ' + memStats.max + '
' + - '
DISK' + makeHistoBar(diskStats, '#803018') + '' + diskStats.cur + ' / ' + diskStats.max + '
' + - '
LOAD' + makeHistoBar({cur: loadStats.cur * 25, max: loadStats.max * 25, avg: loadStats.avg * 25}, '#3D35A0') + '' + (loadStats.cur).toFixed(1) + ' / ' + (loadStats.max).toFixed(1) + '
' + - '
' + - '
■ Current■ Average■ Max
' + - '
' + - '
System
' + - '
Services
' + - '
Security
' + - '
'; + 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 = '
' + + '' + layerIcons[1] + ' ' + layerTitles[1] + ' — ' + healthLabels[hwLevel] + + '
' + + '
' + + '
CPU' + makeHistoBar(cpuStats, '#C04E24') + '' + cpuStats.cur + '% / ' + cpuStats.max + '%
' + + '
MEM' + makeHistoBar(memStats, '#9A6010') + '' + memStats.cur + '% / ' + memStats.max + '%
' + + '
DISK' + makeHistoBar(diskStats, '#803018') + '' + diskStats.cur + '% / ' + diskStats.max + '%
' + + '
LOAD' + makeHistoBar({cur: loadStats.cur * 25, max: loadStats.max * 25, avg: loadStats.avg * 25}, '#3D35A0') + '' + (loadStats.cur).toFixed(1) + ' / ' + (loadStats.max).toFixed(1) + '
' + + '
' + + '
■ Current■ Average■ Max
'; + + } 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 = '
' + + '' + layerIcons[2] + ' ' + layerTitles[2] + ' — ' + healthLabels[svcLevel] + + '
' + + '
' + + '
' + counts.ok + 'OK
' + + '
' + counts.warn + 'WARN
' + + '
' + counts.error + 'ERROR
' + + '
' + counts.unknown + 'UNKNOWN
' + + '
' + + ''; + + } 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 = '
' + + '' + layerIcons[3] + ' ' + layerTitles[3] + ' — ' + secLabel + + '
' + + '
' + + '
🚫' + bans + 'Active Bans
' + + '
⚠️' + alerts + 'Recent Alerts
' + + '
' + + ''; + } 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 = '' + + '
' + + '
--
' + + '
--
' + + '
--
' + + '
' + ''; try { @@ -1368,10 +1475,10 @@ }); sidebar.innerHTML = '