mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 13:59:40 +00:00
feat(hub): Sidebar v2.17.1 AOPC pattern + login fixes
- Sidebar v2.17.1: Async Over-Chronous Pre-Cache pattern - Cache is persistent reference, never discarded - Incremental LED updates, no full re-sort on refresh - Simplified LED update logic - L3 DOM cache layer survives master-top removal - 404/timeout = red error, no fallback - Login: Fixed endpoint /api/v1/hub/auth/login - Index: Fixed redirects to /portal/login.html - Added AOPC pattern documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
da8b7c5570
commit
5b64aa9028
403
.claude/ASYNC-PRECACHE-PATTERN.md
Normal file
403
.claude/ASYNC-PRECACHE-PATTERN.md
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
# Async Pre-Cache Pattern — Development Design
|
||||
|
||||
## SecuBox Health-Aware Navigation Architecture
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Author**: CyberMind / Gérald Kerma
|
||||
**Pattern Name**: Asynchronous Over-Chronous Pre-Cache (AOPC)
|
||||
|
||||
---
|
||||
|
||||
## Philosophy
|
||||
|
||||
> "Pre-cache for speed, refresh for truth."
|
||||
|
||||
Two complementary layers working asynchronously over time:
|
||||
|
||||
| Layer | Purpose | Speed | Accuracy | Blocking |
|
||||
|-------|---------|-------|----------|----------|
|
||||
| **Pre-cache** | Instant UX | Fast (0ms) | Stale (≤TTL) | Never |
|
||||
| **Refresh** | Ground truth | Slow (100-4000ms) | Real-time | Never |
|
||||
|
||||
---
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Optimistic Display
|
||||
```
|
||||
User experience > Perfect accuracy
|
||||
Show something immediately, correct it later
|
||||
```
|
||||
|
||||
### 2. Non-Blocking Everything
|
||||
```
|
||||
No await on critical path
|
||||
Background tasks update UI progressively
|
||||
```
|
||||
|
||||
### 3. Graceful Degradation
|
||||
```
|
||||
Missing endpoint → Fallback check
|
||||
Timeout → Use cached value
|
||||
Error → Show last known state
|
||||
```
|
||||
|
||||
### 4. Per-Item Timestamps
|
||||
```
|
||||
Each module has its own staleness clock
|
||||
Refresh only what's stale, not everything
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PAGE LOAD │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ LAYER 1: PRE-CACHE (Synchronous Read) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ localStorage.getItem('health_cache') │ │
|
||||
│ │ ├─ Parse JSON │ │
|
||||
│ │ ├─ Filter by TTL (discard expired modules) │ │
|
||||
│ │ └─ Return valid cached health states │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ INSTANT RENDER │ │
|
||||
│ │ └─ Display LEDs from pre-cache (T=0ms) │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ (non-blocking)
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ LAYER 2: REFRESH (Asynchronous Background) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Promise.allSettled(modules.map(checkHealth)) │ │
|
||||
│ │ ├─ Each module checks independently │ │
|
||||
│ │ ├─ Timeout: 4s per module │ │
|
||||
│ │ ├─ Fallback: HEAD request to module page │ │
|
||||
│ │ └─ Update LED as each result arrives │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ PROGRESSIVE UPDATE │ │
|
||||
│ │ ├─ LED updates individually (T=100-4000ms) │ │
|
||||
│ │ ├─ Sort order updates after batch complete │ │
|
||||
│ │ └─ Save new timestamps to pre-cache │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ (interval: 30s)
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ LAYER 3: STALE REFRESH (Periodic Background) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ For each module: │ │
|
||||
│ │ ├─ Check timestamp age │ │
|
||||
│ │ ├─ If age > REFRESH_INTERVAL → Re-check │ │
|
||||
│ │ └─ Skip fresh modules (bandwidth efficient) │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Structures
|
||||
|
||||
### Pre-Cache Schema (localStorage)
|
||||
|
||||
```javascript
|
||||
{
|
||||
"module_name": {
|
||||
"status": "ok" | "warn" | "error" | "unknown",
|
||||
"msg": "Human readable message",
|
||||
"data": { /* optional extra data */ },
|
||||
"timestamp": 1778237632000, // Per-module timestamp
|
||||
"fallback": true | false // Was this from page check?
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Constants
|
||||
|
||||
```javascript
|
||||
const HEALTH_CACHE_KEY = 'sbx_health_cache';
|
||||
const HEALTH_CACHE_TTL = 120000; // 2 minutes max staleness
|
||||
const HEALTH_REFRESH_INTERVAL = 30000; // 30s between refreshes
|
||||
const HEALTH_REQUEST_TIMEOUT = 4000; // 4s per module timeout
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Patterns
|
||||
|
||||
### Pattern 1: Instant Pre-Cache Load
|
||||
|
||||
```javascript
|
||||
function loadPreCache() {
|
||||
try {
|
||||
const cached = localStorage.getItem(HEALTH_CACHE_KEY);
|
||||
if (!cached) return null;
|
||||
|
||||
const data = JSON.parse(cached);
|
||||
const now = Date.now();
|
||||
const valid = {};
|
||||
|
||||
for (const mod in data) {
|
||||
const age = now - (data[mod].timestamp || 0);
|
||||
if (age < HEALTH_CACHE_TTL) {
|
||||
valid[mod] = data[mod];
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(valid).length > 0 ? valid : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Non-Blocking Health Check
|
||||
|
||||
```javascript
|
||||
async function checkModuleHealth(mod) {
|
||||
const endpoint = MODULE_HEALTH[mod];
|
||||
|
||||
// Prevent duplicate concurrent checks
|
||||
if (inProgress[mod]) {
|
||||
return cache[mod] || { status: 'checking' };
|
||||
}
|
||||
inProgress[mod] = true;
|
||||
|
||||
try {
|
||||
const ctrl = new AbortController();
|
||||
const timeout = setTimeout(() => ctrl.abort(), TIMEOUT);
|
||||
|
||||
const res = await fetch(endpoint, { signal: ctrl.signal });
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
return { status: data.status, timestamp: Date.now() };
|
||||
}
|
||||
|
||||
// 404 = No health endpoint, try fallback
|
||||
if (res.status === 404) {
|
||||
return await checkPageFallback(mod);
|
||||
}
|
||||
|
||||
return { status: 'error', msg: 'HTTP ' + res.status };
|
||||
} catch (e) {
|
||||
if (e.name === 'AbortError') {
|
||||
return await checkPageFallback(mod);
|
||||
}
|
||||
return { status: 'error', msg: 'Failed' };
|
||||
} finally {
|
||||
inProgress[mod] = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Fallback Page Check
|
||||
|
||||
```javascript
|
||||
async function checkPageFallback(mod) {
|
||||
const page = '/' + mod + '/';
|
||||
|
||||
try {
|
||||
const res = await fetch(page, {
|
||||
method: 'HEAD',
|
||||
timeout: 2000
|
||||
});
|
||||
|
||||
if (res.ok || res.status === 401) {
|
||||
// Page exists (401 = needs auth but exists)
|
||||
return {
|
||||
status: 'ok',
|
||||
msg: 'Page accessible',
|
||||
fallback: true
|
||||
};
|
||||
}
|
||||
return { status: 'unknown', msg: 'No health API' };
|
||||
} catch (e) {
|
||||
return { status: 'unknown', msg: 'Unreachable' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Stale-Only Refresh
|
||||
|
||||
```javascript
|
||||
async function refreshStaleHealth() {
|
||||
const now = Date.now();
|
||||
const stale = [];
|
||||
|
||||
// Identify stale modules only
|
||||
for (const mod in MODULE_HEALTH) {
|
||||
const cached = healthCache[mod];
|
||||
const age = now - (cached?.timestamp || 0);
|
||||
if (age > REFRESH_INTERVAL) {
|
||||
stale.push(mod);
|
||||
}
|
||||
}
|
||||
|
||||
if (stale.length === 0) return;
|
||||
|
||||
// Refresh stale modules in parallel
|
||||
await Promise.allSettled(stale.map(async (mod) => {
|
||||
const result = await checkModuleHealth(mod);
|
||||
healthCache[mod] = result;
|
||||
|
||||
// Update UI immediately as each result arrives
|
||||
updateSingleLED(mod, result);
|
||||
}));
|
||||
|
||||
// Persist to pre-cache
|
||||
savePreCache(healthCache);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 5: Progressive LED Update
|
||||
|
||||
```javascript
|
||||
function updateSingleLED(mod, health) {
|
||||
const item = document.querySelector(`[data-module="${mod}"]`);
|
||||
const led = item?.querySelector('.status-led');
|
||||
if (!led) return;
|
||||
|
||||
// Update LED emoji
|
||||
led.textContent = LED_EMOJI[health.status] || '⚫';
|
||||
led.title = health.msg;
|
||||
|
||||
// Animate if status changed significantly
|
||||
if (health.status === 'ok' && item.classList.contains('offline')) {
|
||||
item.classList.add('reappear');
|
||||
setTimeout(() => item.classList.remove('reappear'), 600);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timeline Example
|
||||
|
||||
```
|
||||
T=0ms Page loads
|
||||
└─ Pre-cache read: {hub: ok, waf: ok, system: warn} (30s old)
|
||||
└─ LEDs display: 🟢 🟢 🟡
|
||||
|
||||
T=10ms Background refresh starts (non-blocking)
|
||||
└─ fetch('/api/v1/hub/health')
|
||||
└─ fetch('/api/v1/waf/health')
|
||||
└─ fetch('/api/v1/system/health')
|
||||
|
||||
T=150ms Hub responds first
|
||||
└─ LED updates: 🟢 (confirmed)
|
||||
|
||||
T=800ms WAF responds
|
||||
└─ LED updates: 🟢 (confirmed)
|
||||
|
||||
T=2500ms System responds (slow)
|
||||
└─ LED updates: 🟢 (was warn, now ok!)
|
||||
|
||||
T=30000ms Stale refresh cycle
|
||||
└─ Check: hub (age: 30s) → refresh
|
||||
└─ Skip: waf (age: 29s) → still fresh
|
||||
└─ Skip: system (age: 27s) → still fresh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Matrix
|
||||
|
||||
| Scenario | Pre-Cache | Refresh | Result |
|
||||
|----------|-----------|---------|--------|
|
||||
| Fresh cache, API ok | ✅ Show cached | ✅ Confirm | 🟢 Instant + verified |
|
||||
| Fresh cache, API timeout | ✅ Show cached | ⚠️ Fallback | 🟢 Cached (may be stale) |
|
||||
| Fresh cache, API 404 | ✅ Show cached | ⚠️ Page check | 🟢 or ⚫ depends on page |
|
||||
| Stale cache, API ok | ⚠️ Show stale | ✅ Update | 🟢 Corrected |
|
||||
| No cache, API ok | ⚫ Show unknown | ✅ Update | 🟢 Discovered |
|
||||
| No cache, API fail | ⚫ Show unknown | ❌ Fallback | ⚫ or 🔴 |
|
||||
|
||||
---
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Zero perceived latency** — User sees status instantly
|
||||
2. **Eventually consistent** — Truth arrives within seconds
|
||||
3. **Bandwidth efficient** — Only refresh stale items
|
||||
4. **Fault tolerant** — Graceful fallbacks at every layer
|
||||
5. **Progressive enhancement** — Works even with missing APIs
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
```javascript
|
||||
// ❌ BAD: Blocking wait for all health checks
|
||||
const health = await Promise.all(modules.map(check));
|
||||
renderSidebar(health); // User waits 4+ seconds
|
||||
|
||||
// ✅ GOOD: Pre-cache first, refresh in background
|
||||
const cached = loadPreCache();
|
||||
renderSidebar(cached); // Instant
|
||||
checkAllHealth().then(updateLEDs); // Background
|
||||
```
|
||||
|
||||
```javascript
|
||||
// ❌ BAD: Single timestamp for entire cache
|
||||
cache = { data: {...}, timestamp: Date.now() }
|
||||
|
||||
// ✅ GOOD: Per-module timestamps
|
||||
cache = {
|
||||
hub: { status: 'ok', timestamp: T1 },
|
||||
waf: { status: 'ok', timestamp: T2 } // Different freshness
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// ❌ BAD: Refresh everything every cycle
|
||||
setInterval(() => checkAllModules(), 30000);
|
||||
|
||||
// ✅ GOOD: Refresh only stale modules
|
||||
setInterval(() => refreshStaleOnly(), 30000);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Sidebar Navigation
|
||||
- Pre-cache on page load
|
||||
- Background refresh on mount
|
||||
- Interval refresh every 30s
|
||||
- LED updates as results arrive
|
||||
|
||||
### Module Dashboards
|
||||
- Can contribute to shared pre-cache
|
||||
- Read pre-cache for cross-module status
|
||||
- Push updates to cache on local health changes
|
||||
|
||||
### Error Pages
|
||||
- Read pre-cache to show system status
|
||||
- Trigger refresh to verify current state
|
||||
|
||||
---
|
||||
|
||||
## Future Extensions
|
||||
|
||||
1. **WebSocket layer** — Real-time push updates
|
||||
2. **Service Worker** — Offline-first caching
|
||||
3. **Shared Worker** — Cross-tab cache synchronization
|
||||
4. **IndexedDB** — Larger cache with history
|
||||
|
||||
---
|
||||
|
||||
*This pattern enables instant UI responsiveness while maintaining factual accuracy through asynchronous background verification.*
|
||||
|
|
@ -4,6 +4,94 @@
|
|||
---
|
||||
## 2026-05-08
|
||||
|
||||
### Session 123 — Health-Aware Sidebar, WAF Alerting & ACME Certs
|
||||
|
||||
**Sidebar v2.0.0 (Emoji LED + Pre-cache):**
|
||||
- Emoji LED status: 🟢ok 🟡warn 🔴error ⚫unknown 🔵checking
|
||||
- Auto-sort: healthy first (ok → warn → unknown → error)
|
||||
- Pre-cache in localStorage for instant display on load
|
||||
- Quick error toast (no buttons, auto-dismiss 2.5s)
|
||||
- Pre-flight health checks on navigation
|
||||
- 30-second periodic health refresh
|
||||
|
||||
**WAF WebUI Alerting:**
|
||||
- Live threat ticker with pulsing red indicator
|
||||
- 7680 total threats, 2813+ today detected
|
||||
- Alerting tab with filterable list (severity, category, IP)
|
||||
- Export alerts to CSV
|
||||
- Quick ban buttons on each alert
|
||||
- Compact category listing with emoji + toggle
|
||||
|
||||
**WAF Threat Log Fix:**
|
||||
- Symlinked `/var/log/secubox/waf-threats.log` → `/srv/mitmproxy/logs/waf-threats.log`
|
||||
- Mitmproxy logs now accessible to WAF API
|
||||
|
||||
**LXC Network Fix:**
|
||||
- Updated `lxc-network-fix.service` to run continuously (30s loop)
|
||||
- Fixed all container symlinks in `/var/lib/lxc/`
|
||||
- Disabled `cgroup2.cpu.max` in all container configs
|
||||
|
||||
**Certificate Status (CRITICAL):**
|
||||
- 15+ certificates EXPIRED
|
||||
- 40+ expiring within 30 days
|
||||
- Certificates at `/data/haproxy/certs/`
|
||||
|
||||
**ACME Certificate Manager WebUI:**
|
||||
- Created `/certs/` dashboard with emoji TTD indicators
|
||||
- 💀expired 🔴critical 🟡warning 🟢healthy
|
||||
- Pre-flight wizard with DNS/HTTP/ACME checks
|
||||
- Origin emoji icons per backend type
|
||||
|
||||
**Files Deployed:**
|
||||
- `/usr/share/secubox/www/shared/sidebar.js` — v2.0.0 pre-cache
|
||||
- `/usr/share/secubox/www/waf/index.html` — alerting system
|
||||
- `/usr/share/secubox/www/certs/index.html` — ACME manager
|
||||
- `/etc/systemd/system/lxc-network-fix.service` — continuous veth fix
|
||||
|
||||
---
|
||||
|
||||
### Session 122 — WAF Architecture Fix & Eye Remote Integration
|
||||
|
||||
**mitmproxy LXC Container Fix:**
|
||||
- Container was STOPPED causing 503 on all vhosts
|
||||
- Fixed cgroup2.cpu.max config preventing startup
|
||||
- Created `lxc-network-fix.service` to auto-start veth interfaces
|
||||
- mitmproxy now running with 145 routes and 150 WAF rules
|
||||
|
||||
**HAProxy Routing Refactor:**
|
||||
- Changed all vhosts from `nginx_vhosts` → `mitmproxy_inspector`
|
||||
- All HTTP traffic now flows through WAF inspection
|
||||
- Fallback backend changed from 503 deny to mitmproxy pass-through
|
||||
- Traffic flow: `HAProxy → mitmproxy (WAF) → nginx/backends`
|
||||
|
||||
**Eye Remote Dashboard:**
|
||||
- Fixed `/eye-remote/` page (nginx location was missing)
|
||||
- Simplified JS to work with pizero-metrics API
|
||||
- Added Quick Commands mini card
|
||||
- API proxy: `/api/v1/eye-remote/*` → `10.55.0.2:8000/*`
|
||||
|
||||
**USB Gadget Network:**
|
||||
- usb0 interface at 10.55.0.1/24
|
||||
- Pi Zero W responding at 10.55.0.2:8000
|
||||
- Live metrics: CPU, RAM, Temp, Uptime, Load
|
||||
|
||||
**CrowdSec Status:**
|
||||
- 100+ active bans (SSH brute-force from DE/NL/RO/SE)
|
||||
- WAF threats log ready at `/srv/mitmproxy/logs/waf-threats.log`
|
||||
|
||||
**Dependencies Added:**
|
||||
- `netcat-openbsd` for diagnostics
|
||||
|
||||
**Files Modified:**
|
||||
- `/etc/haproxy/haproxy.cfg` — All vhosts through mitmproxy
|
||||
- `/etc/nginx/sites-enabled/webui.conf` — Eye Remote locations
|
||||
- `/var/lib/lxc/mitmproxy/config` — Fixed network config
|
||||
- `/etc/systemd/system/lxc-network-fix.service` — Auto veth startup
|
||||
- `/usr/share/secubox/www/eye-remote/index.html` — Simplified dashboard
|
||||
- `/usr/share/secubox/www/eye-remote/js/eye-remote.js` — pizero-metrics API
|
||||
|
||||
---
|
||||
|
||||
### Session 121 — HealthBump v2.1 with Activity Detection & K2000
|
||||
|
||||
**Features Added:**
|
||||
|
|
|
|||
|
|
@ -671,7 +671,7 @@
|
|||
|
||||
function checkAuth() {
|
||||
if (!getToken()) {
|
||||
window.location.href = '/login.html?redirect=' + encodeURIComponent(window.location.pathname);
|
||||
window.location.href = '/portal/login.html?redirect=' + encodeURIComponent(window.location.pathname);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -888,7 +888,7 @@
|
|||
await fetch('/api/v1/portal/logout', { method: 'POST', credentials: 'include' });
|
||||
localStorage.removeItem('secubox_token');
|
||||
localStorage.removeItem('sbx_token');
|
||||
window.location.href = '/login.html';
|
||||
window.location.href = '/portal/login.html';
|
||||
}
|
||||
|
||||
async function loadNetworkMode() {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,27 @@ async function callPublicIPs(params) {
|
|||
return sbxFetch('/api/v1/hub/get_public_ips', params, 'GET');
|
||||
}
|
||||
|
||||
// Eye Remote USB Gadget API
|
||||
async function callEyeRemoteMetrics() {
|
||||
try {
|
||||
var response = await fetch('http://10.55.0.2:8000/metrics', { timeout: 3000 });
|
||||
if (!response.ok) throw new Error('Not available');
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function callEyeRemoteHealth() {
|
||||
try {
|
||||
var response = await fetch('http://10.55.0.2:8000/health', { timeout: 2000 });
|
||||
if (!response.ok) throw new Error('Not available');
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
|
|
@ -66,7 +87,8 @@ return view.extend({
|
|||
alerts: [],
|
||||
publicIPs: {},
|
||||
board: {},
|
||||
info: {}
|
||||
info: {},
|
||||
eyeRemote: null
|
||||
},
|
||||
|
||||
load: function() {
|
||||
|
|
@ -78,7 +100,8 @@ return view.extend({
|
|||
callSystemHealth().catch(function() { return {}; }),
|
||||
callGetModules().catch(function() { return { modules: [] }; }),
|
||||
callGetAlerts().catch(function() { return { alerts: [] }; }),
|
||||
callPublicIPs().catch(function() { return {}; })
|
||||
callPublicIPs().catch(function() { return {}; }),
|
||||
callEyeRemoteMetrics().catch(function() { return null; })
|
||||
]).then(function(results) {
|
||||
self.data = {
|
||||
board: results[0] || {},
|
||||
|
|
@ -87,7 +110,8 @@ return view.extend({
|
|||
health: results[3] || {},
|
||||
modules: (results[4] && results[4].modules) || [],
|
||||
alerts: (results[5] && results[5].alerts) || [],
|
||||
publicIPs: results[6] || {}
|
||||
publicIPs: results[6] || {},
|
||||
eyeRemote: results[7]
|
||||
};
|
||||
return self.data;
|
||||
});
|
||||
|
|
@ -252,6 +276,48 @@ return view.extend({
|
|||
}));
|
||||
},
|
||||
|
||||
renderEyeRemote: function() {
|
||||
var c = KissTheme.colors;
|
||||
var eye = this.data.eyeRemote;
|
||||
|
||||
if (!eye) {
|
||||
return E('div', { 'id': 'eye-remote-content', 'style': 'text-align: center; padding: 20px; color: var(--kiss-muted);' }, [
|
||||
E('div', { 'style': 'font-size: 24px; margin-bottom: 8px;' }, '\u{1F441}'),
|
||||
E('div', {}, 'Eye Remote not connected'),
|
||||
E('div', { 'style': 'font-size: 11px; margin-top: 8px;' }, 'USB: 10.55.0.2')
|
||||
]);
|
||||
}
|
||||
|
||||
var cpuColor = eye.cpu_percent > 80 ? c.red : eye.cpu_percent > 50 ? c.orange : c.green;
|
||||
var tempColor = eye.cpu_temp > 70 ? c.red : eye.cpu_temp > 55 ? c.orange : c.green;
|
||||
var memColor = eye.mem_percent > 80 ? c.red : eye.mem_percent > 60 ? c.orange : c.green;
|
||||
|
||||
return E('div', { 'id': 'eye-remote-content' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 8px; margin-bottom: 12px;' }, [
|
||||
E('span', { 'style': 'font-size: 20px;' }, '\u{1F441}'),
|
||||
E('span', { 'style': 'font-weight: 600;' }, eye.hostname || 'eye-remote'),
|
||||
KissTheme.badge('Online', 'green')
|
||||
]),
|
||||
E('div', { 'style': 'display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;' }, [
|
||||
E('div', { 'style': 'text-align: center; padding: 10px; background: var(--kiss-bg2); border-radius: 6px;' }, [
|
||||
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); text-transform: uppercase;' }, 'CPU'),
|
||||
E('div', { 'style': 'font-size: 18px; font-weight: bold; color: ' + cpuColor + ';', 'data-eye': 'cpu' }, eye.cpu_percent.toFixed(0) + '%')
|
||||
]),
|
||||
E('div', { 'style': 'text-align: center; padding: 10px; background: var(--kiss-bg2); border-radius: 6px;' }, [
|
||||
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Temp'),
|
||||
E('div', { 'style': 'font-size: 18px; font-weight: bold; color: ' + tempColor + ';', 'data-eye': 'temp' }, eye.cpu_temp.toFixed(1) + '\u00B0C')
|
||||
]),
|
||||
E('div', { 'style': 'text-align: center; padding: 10px; background: var(--kiss-bg2); border-radius: 6px;' }, [
|
||||
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Mem'),
|
||||
E('div', { 'style': 'font-size: 18px; font-weight: bold; color: ' + memColor + ';', 'data-eye': 'mem' }, eye.mem_percent.toFixed(0) + '%')
|
||||
])
|
||||
]),
|
||||
E('div', { 'style': 'margin-top: 10px; font-size: 11px; color: var(--kiss-muted); text-align: center;', 'data-eye': 'uptime' },
|
||||
'Uptime: ' + formatUptime(eye.uptime_seconds) + ' \u2022 Load: ' + (eye.load_1m || 0).toFixed(2)
|
||||
)
|
||||
]);
|
||||
},
|
||||
|
||||
renderAlerts: function() {
|
||||
var c = KissTheme.colors;
|
||||
var alerts = (this.data.alerts || []).slice(0, 5);
|
||||
|
|
@ -310,11 +376,13 @@ return view.extend({
|
|||
return Promise.all([
|
||||
callSystemHealth().catch(function() { return {}; }),
|
||||
callGetAlerts().catch(function() { return { alerts: [] }; }),
|
||||
callPublicIPs().catch(function() { return {}; })
|
||||
callPublicIPs().catch(function() { return {}; }),
|
||||
callEyeRemoteMetrics().catch(function() { return null; })
|
||||
]).then(function(results) {
|
||||
self.data.health = results[0] || {};
|
||||
self.data.alerts = (results[1] && results[1].alerts) || [];
|
||||
self.data.publicIPs = results[2] || {};
|
||||
self.data.eyeRemote = results[3];
|
||||
self.updateLiveData();
|
||||
});
|
||||
}, 15);
|
||||
|
|
@ -352,6 +420,9 @@ return view.extend({
|
|||
|
||||
// Right column
|
||||
E('div', { 'style': 'display: flex; flex-direction: column; gap: 20px;' }, [
|
||||
// Eye Remote USB Gadget
|
||||
KissTheme.card('Eye Remote', this.renderEyeRemote()),
|
||||
|
||||
// Network Addresses
|
||||
KissTheme.card('Network Addresses', this.renderNetworkAddresses()),
|
||||
|
||||
|
|
@ -407,6 +478,36 @@ return view.extend({
|
|||
alertsEl.innerHTML = '';
|
||||
alertsEl.appendChild(this.renderAlerts());
|
||||
}
|
||||
|
||||
// Update Eye Remote panel
|
||||
var eyeEl = document.getElementById('eye-remote-content');
|
||||
if (eyeEl) {
|
||||
var eye = this.data.eyeRemote;
|
||||
if (eye) {
|
||||
var cpuEl = document.querySelector('[data-eye="cpu"]');
|
||||
var tempEl = document.querySelector('[data-eye="temp"]');
|
||||
var memEl = document.querySelector('[data-eye="mem"]');
|
||||
var uptimeEl = document.querySelector('[data-eye="uptime"]');
|
||||
if (cpuEl) {
|
||||
cpuEl.textContent = eye.cpu_percent.toFixed(0) + '%';
|
||||
cpuEl.style.color = eye.cpu_percent > 80 ? c.red : eye.cpu_percent > 50 ? c.orange : c.green;
|
||||
}
|
||||
if (tempEl) {
|
||||
tempEl.textContent = eye.cpu_temp.toFixed(1) + '\u00B0C';
|
||||
tempEl.style.color = eye.cpu_temp > 70 ? c.red : eye.cpu_temp > 55 ? c.orange : c.green;
|
||||
}
|
||||
if (memEl) {
|
||||
memEl.textContent = eye.mem_percent.toFixed(0) + '%';
|
||||
memEl.style.color = eye.mem_percent > 80 ? c.red : eye.mem_percent > 60 ? c.orange : c.green;
|
||||
}
|
||||
if (uptimeEl) {
|
||||
uptimeEl.textContent = 'Uptime: ' + formatUptime(eye.uptime_seconds) + ' \u2022 Load: ' + (eye.load_1m || 0).toFixed(2);
|
||||
}
|
||||
} else {
|
||||
eyeEl.parentElement.innerHTML = '';
|
||||
eyeEl.parentElement.appendChild(this.renderEyeRemote());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleSave: null,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user