diff --git a/LUCI_DEVELOPMENT_REFERENCE.md b/LUCI_DEVELOPMENT_REFERENCE.md new file mode 100644 index 0000000..bf7b8b0 --- /dev/null +++ b/LUCI_DEVELOPMENT_REFERENCE.md @@ -0,0 +1,1181 @@ +# LuCI Development Reference Guide + +**Version**: 1.0 +**Date**: 2025-12-26 +**Based on**: luci-app-secubox and luci-app-system-hub implementations +**Target Audience**: Claude.ai and developers working on OpenWrt LuCI applications + +This document captures critical patterns, best practices, and common pitfalls discovered during the development of SecuBox LuCI applications. Use this as a validation reference for all future LuCI application development. + +--- + +## Table of Contents + +1. [ubus and RPC Fundamentals](#ubus-and-rpc-fundamentals) +2. [RPCD Backend Patterns](#rpcd-backend-patterns) +3. [LuCI API Module Patterns](#luci-api-module-patterns) +4. [LuCI View Import Patterns](#luci-view-import-patterns) +5. [ACL Permission Structure](#acl-permission-structure) +6. [Data Structure Conventions](#data-structure-conventions) +7. [Common Errors and Solutions](#common-errors-and-solutions) +8. [Validation Checklist](#validation-checklist) +9. [Testing and Deployment](#testing-and-deployment) + +--- + +## ubus and RPC Fundamentals + +### What is ubus? + +**ubus** (OpenWrt micro bus architecture) is OpenWrt's inter-process communication (IPC) system. It enables: +- RPC (Remote Procedure Call) between processes +- Web interface (LuCI) to backend service communication +- Command-line interaction via `ubus call` + +### ubus Object Naming Convention + +**CRITICAL RULE**: All LuCI application ubus objects MUST use the `luci.` prefix. + +```javascript +// ✅ CORRECT +object: 'luci.system-hub' +object: 'luci.cdn-cache' +object: 'luci.wireguard-dashboard' + +// ❌ WRONG +object: 'system-hub' +object: 'systemhub' +object: 'cdn-cache' +``` + +**Why?** LuCI expects objects under the `luci.*` namespace for web applications. Without this prefix: +- ACL permissions won't match +- RPCD won't route calls correctly +- Browser console shows: `RPC call to system-hub/status failed with error -32000: Object not found` + +### RPCD Script Naming MUST Match ubus Object + +The RPCD script filename MUST exactly match the ubus object name: + +```bash +# If JavaScript declares: +# object: 'luci.system-hub' + +# Then RPCD script MUST be named: +/usr/libexec/rpcd/luci.system-hub + +# NOT: +/usr/libexec/rpcd/system-hub +/usr/libexec/rpcd/luci-system-hub +``` + +**Validation Command**: +```bash +# Check JavaScript files for ubus object names +grep -r "object:" luci-app-*/htdocs --include="*.js" + +# Verify RPCD script exists with matching name +ls luci-app-*/root/usr/libexec/rpcd/ +``` + +### ubus Call Types + +**Read Operations** (GET-like): +- `status` - Get current state +- `get_*` - Retrieve data (e.g., `get_health`, `get_settings`) +- `list_*` - Enumerate items (e.g., `list_services`) + +**Write Operations** (POST-like): +- `save_*` - Persist configuration (e.g., `save_settings`) +- `*_action` - Perform actions (e.g., `service_action`) +- `backup`, `restore`, `reboot` - System modifications + +**ACL Mapping**: +- Read operations → `"read"` section in ACL +- Write operations → `"write"` section in ACL + +--- + +## RPCD Backend Patterns + +### Shell Script Structure + +RPCD backends are executable shell scripts that: +1. Parse `$1` for the action (`list` or `call`) +2. Parse `$2` for the method name (if `call`) +3. Read JSON input from stdin (for methods with parameters) +4. Output JSON to stdout +5. Exit with status 0 on success, non-zero on error + +### Standard Template + +```bash +#!/bin/sh +# RPCD Backend: luci.system-hub +# Version: 0.1.0 + +# Source JSON shell helper +. /usr/share/libubox/jshn.sh + +case "$1" in + list) + # List all available methods and their parameters + echo '{ + "status": {}, + "get_health": {}, + "service_action": { "service": "string", "action": "string" }, + "save_settings": { + "auto_refresh": 0, + "health_check": 0, + "refresh_interval": 0 + } + }' + ;; + call) + case "$2" in + status) + status + ;; + get_health) + get_health + ;; + service_action) + # Read JSON input from stdin + read -r input + json_load "$input" + json_get_var service service + json_get_var action action + service_action "$service" "$action" + ;; + save_settings) + read -r input + json_load "$input" + json_get_var auto_refresh auto_refresh + json_get_var health_check health_check + json_get_var refresh_interval refresh_interval + save_settings "$auto_refresh" "$health_check" "$refresh_interval" + ;; + *) + echo '{"error": "Method not found"}' + exit 1 + ;; + esac + ;; +esac +``` + +### JSON Output with jshn.sh + +**jshn.sh** provides shell functions for JSON manipulation: + +```bash +# Initialize JSON object +json_init + +# Add simple values +json_add_string "hostname" "openwrt" +json_add_int "uptime" 86400 +json_add_boolean "running" 1 + +# Add nested object +json_add_object "cpu" +json_add_int "usage" 25 +json_add_string "status" "ok" +json_close_object + +# Add array +json_add_array "services" +json_add_string "" "network" +json_add_string "" "firewall" +json_close_array + +# Output JSON to stdout +json_dump +``` + +**Common Functions**: +- `json_init` - Start new JSON object +- `json_add_string "key" "value"` - Add string +- `json_add_int "key" 123` - Add integer +- `json_add_boolean "key" 1` - Add boolean (0 or 1) +- `json_add_object "key"` - Start nested object +- `json_close_object` - End nested object +- `json_add_array "key"` - Start array +- `json_close_array` - End array +- `json_dump` - Output JSON to stdout + +### Error Handling + +Always validate inputs and return meaningful errors: + +```bash +service_action() { + local service="$1" + local action="$2" + + # Validate service name + if [ -z "$service" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Service name is required" + json_dump + return 1 + fi + + # Validate action + case "$action" in + start|stop|restart|enable|disable) + ;; + *) + json_init + json_add_boolean "success" 0 + json_add_string "error" "Invalid action: $action" + json_dump + return 1 + ;; + esac + + # Perform action + /etc/init.d/"$service" "$action" >/dev/null 2>&1 + + if [ $? -eq 0 ]; then + json_init + json_add_boolean "success" 1 + json_add_string "message" "Service $service $action successful" + json_dump + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "Service $service $action failed" + json_dump + return 1 + fi +} +``` + +### UCI Integration + +For persistent configuration, use UCI (Unified Configuration Interface): + +```bash +save_settings() { + local auto_refresh="$1" + local health_check="$2" + local refresh_interval="$3" + + # Create/update UCI config + uci set system-hub.general=general + uci set system-hub.general.auto_refresh="$auto_refresh" + uci set system-hub.general.health_check="$health_check" + uci set system-hub.general.refresh_interval="$refresh_interval" + uci commit system-hub + + json_init + json_add_boolean "success" 1 + json_add_string "message" "Settings saved successfully" + json_dump +} + +get_settings() { + # Load UCI config + if [ -f "/etc/config/system-hub" ]; then + . /lib/functions.sh + config_load system-hub + fi + + json_init + json_add_object "general" + + # Get value or use default + config_get auto_refresh general auto_refresh "1" + json_add_boolean "auto_refresh" "${auto_refresh:-1}" + + config_get refresh_interval general refresh_interval "30" + json_add_int "refresh_interval" "${refresh_interval:-30}" + + json_close_object + json_dump +} +``` + +### Performance Tips + +1. **Cache expensive operations**: Don't re-read `/proc` files multiple times +2. **Use command substitution efficiently**: + ```bash + # Good + uptime=$(cat /proc/uptime | cut -d' ' -f1) + + # Better + read uptime _ < /proc/uptime + uptime=${uptime%.*} + ``` +3. **Avoid external commands when possible**: + ```bash + # Slow + count=$(ls /etc/init.d | wc -l) + + # Fast + count=0 + for file in /etc/init.d/*; do + [ -f "$file" ] && count=$((count + 1)) + done + ``` + +--- + +## LuCI API Module Patterns + +### CRITICAL: Use baseclass.extend() + +**RULE**: LuCI API modules MUST use `baseclass.extend()` pattern. + +```javascript +'use strict'; +'require baseclass'; +'require rpc'; + +// Declare RPC methods +var callStatus = rpc.declare({ + object: 'luci.system-hub', + method: 'status', + expect: {} +}); + +var callGetHealth = rpc.declare({ + object: 'luci.system-hub', + method: 'get_health', + expect: {} +}); + +var callSaveSettings = rpc.declare({ + object: 'luci.system-hub', + method: 'save_settings', + params: ['auto_refresh', 'health_check', 'refresh_interval'], + expect: {} +}); + +// ✅ CORRECT: Use baseclass.extend() +return baseclass.extend({ + getStatus: callStatus, + getHealth: callGetHealth, + saveSettings: callSaveSettings +}); + +// ❌ WRONG: Do NOT use these patterns +return baseclass.singleton({...}); // Breaks everything! +return {...}; // Plain object doesn't work +``` + +**Why baseclass.extend()?** +- LuCI's module system expects class-based modules +- Views import with `'require module/api as API'` which auto-instantiates +- `baseclass.extend()` creates a proper class constructor +- `baseclass.singleton()` breaks the instantiation mechanism +- Plain objects don't support LuCI's module lifecycle + +### rpc.declare() Parameters + +```javascript +var callMethodName = rpc.declare({ + object: 'luci.module-name', // ubus object name (MUST start with luci.) + method: 'method_name', // RPCD method name + params: ['param1', 'param2'], // Optional: parameter names (order matters!) + expect: {} // Expected return structure (or { key: [] } for arrays) +}); +``` + +**Parameter Order Matters**: +```javascript +// RPCD expects parameters in this exact order +var callSaveSettings = rpc.declare({ + object: 'luci.system-hub', + method: 'save_settings', + params: ['auto_refresh', 'health_check', 'debug_mode', 'refresh_interval'], + expect: {} +}); + +// JavaScript call MUST pass parameters in same order +API.saveSettings(1, 1, 0, 30); // auto_refresh=1, health_check=1, debug_mode=0, refresh_interval=30 +``` + +### expect Parameter Patterns + +```javascript +// Method returns single object +expect: {} + +// Method returns array at top level +expect: { services: [] } + +// Method returns specific structure +expect: { + services: [], + count: 0 +} +``` + +### Error Handling in API Module + +API methods return Promises. Handle errors in views: + +```javascript +return API.getHealth().then(function(data) { + if (!data || typeof data !== 'object') { + console.error('Invalid health data:', data); + return null; + } + return data; +}).catch(function(err) { + console.error('Failed to load health data:', err); + ui.addNotification(null, E('p', {}, 'Failed to load health data'), 'error'); + return null; +}); +``` + +--- + +## LuCI View Import Patterns + +### CRITICAL: Use 'require ... as VAR' for APIs + +**RULE**: When importing API modules, use the `'require ... as VAR'` pattern at the top of the file. + +```javascript +// ✅ CORRECT: Auto-instantiates the class +'require system-hub/api as API'; + +return L.view.extend({ + load: function() { + return API.getHealth(); // API is already instantiated + } +}); + +// ❌ WRONG: Returns class constructor, not instance +var api = L.require('system-hub.api'); +api.getHealth(); // ERROR: api.getHealth is not a function +``` + +**Why?** +- `'require module/path as VAR'` (with forward slashes) auto-instantiates classes +- `L.require('module.path')` (with dots) returns raw class constructor +- API modules extend `baseclass`, which needs instantiation +- LuCI's module loader handles instantiation when using the `as VAR` pattern + +### Standard View Structure + +```javascript +'use strict'; +'require view'; +'require form'; +'require ui'; +'require system-hub/api as API'; + +return L.view.extend({ + load: function() { + // Load data needed for rendering + return Promise.all([ + API.getHealth(), + API.getStatus() + ]); + }, + + render: function(data) { + var health = data[0]; + var status = data[1]; + + // Create UI elements + var container = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, 'Dashboard'), + // ... more elements + ]); + + return container; + }, + + handleSave: null, // Disable save button + handleSaveApply: null, // Disable save & apply button + handleReset: null // Disable reset button +}); +``` + +### Import Patterns Summary + +```javascript +// Core LuCI modules (always with quotes) +'require view'; +'require form'; +'require ui'; +'require rpc'; +'require baseclass'; + +// Custom API modules (use 'as VAR' for auto-instantiation) +'require system-hub/api as API'; +'require cdn-cache/api as CdnAPI'; + +// Access global L object (no require) +L.resolveDefault(...) +L.Poll.add(...) +L.ui.addNotification(...) +``` + +--- + +## ACL Permission Structure + +### File Location + +ACL files are located in: +``` +/usr/share/rpcd/acl.d/luci-app-.json +``` + +In source tree: +``` +luci-app-/root/usr/share/rpcd/acl.d/luci-app-.json +``` + +### Standard ACL Template + +```json +{ + "luci-app-module-name": { + "description": "Module Name - Description", + "read": { + "ubus": { + "luci.module-name": [ + "status", + "get_system_info", + "get_health", + "list_services", + "get_logs", + "get_storage", + "get_settings" + ] + } + }, + "write": { + "ubus": { + "luci.module-name": [ + "service_action", + "backup_config", + "restore_config", + "reboot", + "save_settings" + ] + } + } + } +} +``` + +### Read vs Write Classification + +**Read Operations** (no system modification): +- `status` - Get current state +- `get_*` - Retrieve data (system info, health, settings, logs, storage) +- `list_*` - Enumerate items (services, interfaces, etc.) + +**Write Operations** (modify system state): +- `*_action` - Perform actions (start/stop services, etc.) +- `save_*` - Persist configuration changes +- `backup`, `restore` - System backup/restore +- `reboot`, `shutdown` - System control + +### Common ACL Errors + +**Error**: `Access denied` or RPC error `-32002` + +**Cause**: Method not listed in ACL, or listed in wrong section (read vs write) + +**Solution**: +1. Identify if method is read or write operation +2. Add method name to correct section in ACL +3. Restart RPCD: `/etc/init.d/rpcd restart` + +**Validation**: +```bash +# Check if ACL file is valid JSON +jsonlint /usr/share/rpcd/acl.d/luci-app-system-hub.json + +# List all ubus objects and methods +ubus list luci.system-hub + +# Test method with ubus call +ubus call luci.system-hub get_health +``` + +--- + +## Data Structure Conventions + +### Health Metrics Structure (system-hub v0.1.0) + +Based on extensive iteration, this structure provides clarity and consistency: + +```json +{ + "cpu": { + "usage": 25, + "status": "ok", + "load_1m": "0.25", + "load_5m": "0.30", + "load_15m": "0.28", + "cores": 4 + }, + "memory": { + "total_kb": 4096000, + "free_kb": 2048000, + "available_kb": 3072000, + "used_kb": 1024000, + "buffers_kb": 512000, + "cached_kb": 1536000, + "usage": 25, + "status": "ok" + }, + "disk": { + "total_kb": 30408704, + "used_kb": 5447680, + "free_kb": 24961024, + "usage": 19, + "status": "ok" + }, + "temperature": { + "value": 45, + "status": "ok" + }, + "network": { + "wan_up": true, + "status": "ok" + }, + "services": { + "running": 35, + "failed": 2 + }, + "score": 92, + "timestamp": "2025-12-26 10:30:00", + "recommendations": [ + "2 service(s) enabled but not running. Check service status." + ] +} +``` + +**Key Principles**: +1. **Nested objects** for related metrics (cpu, memory, disk, etc.) +2. **Consistent structure**: Each metric has `usage` (percentage) and `status` (ok/warning/critical) +3. **Raw values + computed values**: Provide both (e.g., `used_kb` AND `usage` percentage) +4. **Status thresholds**: ok (< warning), warning (warning-critical), critical (≥ critical) +5. **Overall score**: Single 0-100 health score for dashboard +6. **Dynamic recommendations**: Array of actionable alerts based on thresholds + +### Status Values + +Use consistent status strings across all metrics: +- `"ok"` - Normal operation (green) +- `"warning"` - Approaching threshold (orange) +- `"critical"` - Exceeded threshold (red) +- `"error"` - Unable to retrieve metric +- `"unknown"` - Metric not available + +### Timestamp Format + +Use ISO 8601 or consistent local format: +```bash +timestamp="$(date '+%Y-%m-%d %H:%M:%S')" # 2025-12-26 10:30:00 +``` + +### Boolean Values in JSON + +In shell scripts using jshn.sh: +```bash +json_add_boolean "wan_up" 1 # true +json_add_boolean "wan_up" 0 # false +``` + +In JavaScript: +```javascript +if (health.network.wan_up) { + // WAN is up +} +``` + +### Array vs Single Value + +**Use arrays for**: +- Multiple items of same type (services, interfaces, mount points) +- Variable-length data + +**Use single values for**: +- System-wide metrics (CPU, memory, disk) +- Primary/aggregate values (overall temperature, total uptime) + +**Example - Storage**: +```json +// Multiple mount points - use array +"storage": [ + { + "mount": "/", + "total_kb": 30408704, + "used_kb": 5447680, + "usage": 19 + }, + { + "mount": "/mnt/usb", + "total_kb": 128000000, + "used_kb": 64000000, + "usage": 50 + } +] + +// Root filesystem only - use object +"disk": { + "total_kb": 30408704, + "used_kb": 5447680, + "usage": 19, + "status": "ok" +} +``` + +--- + +## Common Errors and Solutions + +### 1. RPC Error: "Object not found" (-32000) + +**Error Message**: +``` +RPC call to system-hub/status failed with error -32000: Object not found +``` + +**Cause**: RPCD script name doesn't match ubus object name in JavaScript + +**Solution**: +1. Check JavaScript for object name: + ```bash + grep -r "object:" luci-app-system-hub/htdocs --include="*.js" + ``` + Output: `object: 'luci.system-hub'` + +2. Rename RPCD script to match exactly: + ```bash + mv root/usr/libexec/rpcd/system-hub root/usr/libexec/rpcd/luci.system-hub + ``` + +3. Ensure script is executable: + ```bash + chmod +x root/usr/libexec/rpcd/luci.system-hub + ``` + +4. Restart RPCD: + ```bash + /etc/init.d/rpcd restart + ``` + +### 2. JavaScript Error: "api.methodName is not a function" + +**Error Message**: +``` +Uncaught TypeError: api.getHealth is not a function + at view.load (health.js:12) +``` + +**Cause**: Wrong import pattern - imported class constructor instead of instance + +**Solution**: +Change from: +```javascript +var api = L.require('system-hub.api'); // ❌ Wrong +``` + +To: +```javascript +'require system-hub/api as API'; // ✅ Correct +``` + +**Why**: `L.require('module.path')` returns raw class, `'require module/path as VAR'` auto-instantiates. + +### 3. RPC Error: "Access denied" (-32002) + +**Error Message**: +``` +RPC call to luci.system-hub/get_settings failed with error -32002: Access denied +``` + +**Cause**: Method not listed in ACL file, or in wrong section (read vs write) + +**Solution**: +1. Open ACL file: `root/usr/share/rpcd/acl.d/luci-app-system-hub.json` + +2. Add method to appropriate section: + ```json + "read": { + "ubus": { + "luci.system-hub": [ + "get_settings" + ] + } + } + ``` + +3. Deploy and restart RPCD: + ```bash + scp luci-app-system-hub/root/usr/share/rpcd/acl.d/*.json router:/usr/share/rpcd/acl.d/ + ssh router "/etc/init.d/rpcd restart" + ``` + +### 4. Display Error: "NaN%" or Undefined Values + +**Error**: Dashboard shows "NaN%", "undefined", or empty values + +**Cause**: Frontend using wrong data structure keys (outdated after backend changes) + +**Solution**: +1. Check backend output: + ```bash + ubus call luci.system-hub get_health + ``` + +2. Update frontend to match structure: + ```javascript + // ❌ Old structure + var cpuPercent = health.load / health.cores * 100; + var memPercent = health.memory.percent; + + // ✅ New structure + var cpuPercent = health.cpu ? health.cpu.usage : 0; + var memPercent = health.memory ? health.memory.usage : 0; + ``` + +3. Add null/undefined checks: + ```javascript + var temp = health.temperature?.value || 0; + var loadAvg = health.cpu?.load_1m || '0.00'; + ``` + +### 5. HTTP 404: View File Not Found + +**Error Message**: +``` +HTTP error 404 while loading class file '/luci-static/resources/view/netifyd/overview.js' +``` + +**Cause**: Menu path doesn't match actual view file location + +**Solution**: +1. Check menu JSON: + ```bash + cat root/usr/share/luci/menu.d/luci-app-netifyd-dashboard.json + ``` + Look for: `"path": "netifyd/overview"` + +2. Check actual file location: + ```bash + ls htdocs/luci-static/resources/view/ + ``` + File is at: `view/netifyd-dashboard/overview.js` + +3. Fix either menu path OR file location: + ```json + // Option 1: Update menu path to match file + "path": "netifyd-dashboard/overview" + + // Option 2: Move file to match menu + mv view/netifyd-dashboard/ view/netifyd/ + ``` + +### 6. Build Error: "factory yields invalid constructor" + +**Error Message**: +``` +/luci-static/resources/system-hub/api.js: factory yields invalid constructor +``` + +**Cause**: Used wrong pattern in API module (singleton, plain object, etc.) + +**Solution**: +Always use `baseclass.extend()`: +```javascript +return baseclass.extend({ + getStatus: callStatus, + getHealth: callGetHealth, + // ... more methods +}); +``` + +Do NOT use: +- `baseclass.singleton({...})` +- Plain object: `return {...}` +- `baseclass.prototype` + +### 7. RPCD Not Responding After Changes + +**Symptom**: Changes to RPCD script don't take effect + +**Solution**: +1. Verify script is deployed: + ```bash + ssh router "ls -la /usr/libexec/rpcd/" + ``` + +2. Check script is executable: + ```bash + ssh router "chmod +x /usr/libexec/rpcd/luci.system-hub" + ``` + +3. Restart RPCD: + ```bash + ssh router "/etc/init.d/rpcd restart" + ``` + +4. Clear browser cache (Ctrl+Shift+R) + +5. Check RPCD logs: + ```bash + ssh router "logread | grep rpcd" + ``` + +--- + +## Validation Checklist + +Use this checklist before deployment: + +### File Structure +- [ ] RPCD script exists: `/usr/libexec/rpcd/luci.` +- [ ] RPCD script is executable: `chmod +x` +- [ ] Menu JSON exists: `/usr/share/luci/menu.d/luci-app-.json` +- [ ] ACL JSON exists: `/usr/share/rpcd/acl.d/luci-app-.json` +- [ ] API module exists: `htdocs/luci-static/resources//api.js` +- [ ] Views exist: `htdocs/luci-static/resources/view//*.js` + +### Naming Conventions +- [ ] RPCD script name matches ubus object in JavaScript (including `luci.` prefix) +- [ ] Menu paths match view file directory structure +- [ ] All ubus objects start with `luci.` +- [ ] ACL key matches package name: `"luci-app-"` + +### Code Validation +- [ ] API module uses `baseclass.extend()` pattern +- [ ] Views import API with `'require /api as API'` pattern +- [ ] All rpc.declare() calls include correct `object`, `method`, `params`, `expect` +- [ ] RPCD script outputs valid JSON (test with `ubus call`) +- [ ] Menu JSON is valid (test with `jsonlint`) +- [ ] ACL JSON is valid (test with `jsonlint`) + +### Permissions +- [ ] All read methods in ACL `"read"` section +- [ ] All write methods in ACL `"write"` section +- [ ] Methods in ACL match RPCD script method names exactly + +### Testing +- [ ] Run validation script: `./secubox-tools/validate-modules.sh` +- [ ] Test each method via ubus: `ubus call luci. ` +- [ ] Test frontend in browser (check console for errors) +- [ ] Clear browser cache after deployment +- [ ] Verify RPCD restart: `/etc/init.d/rpcd restart` + +### Automated Validation Command + +```bash +# Run comprehensive validation +./secubox-tools/validate-modules.sh + +# Validate specific module +./secubox-tools/validate-module-generation.sh luci-app-system-hub + +# Check JSON syntax +find luci-app-system-hub -name "*.json" -exec jsonlint {} \; + +# Check shell scripts +shellcheck luci-app-system-hub/root/usr/libexec/rpcd/* +``` + +--- + +## Testing and Deployment + +### Local Testing with ubus + +Before deploying to router, test RPCD script locally: + +```bash +# Copy RPCD script to local /tmp +cp luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub /tmp/ + +# Make executable +chmod +x /tmp/luci.system-hub + +# Test 'list' action +/tmp/luci.system-hub list + +# Test 'call' action with method +/tmp/luci.system-hub call status + +# Test method with parameters +echo '{"service":"network","action":"restart"}' | /tmp/luci.system-hub call service_action +``` + +### Deployment Script + +Use a deployment script for fast iteration: + +```bash +#!/bin/bash +# deploy-system-hub.sh + +ROUTER="root@192.168.8.191" + +echo "🚀 Deploying system-hub to $ROUTER" + +# Deploy API module +scp luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js \ + "$ROUTER:/www/luci-static/resources/system-hub/" + +# Deploy views +scp luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/*.js \ + "$ROUTER:/www/luci-static/resources/view/system-hub/" + +# Deploy RPCD backend +scp luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub \ + "$ROUTER:/usr/libexec/rpcd/" + +# Deploy ACL +scp luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json \ + "$ROUTER:/usr/share/rpcd/acl.d/" + +# Set permissions and restart +ssh "$ROUTER" "chmod +x /usr/libexec/rpcd/luci.system-hub && /etc/init.d/rpcd restart" + +echo "✅ Deployment complete! Clear browser cache (Ctrl+Shift+R)" +``` + +### Browser Testing + +1. Open browser console (F12) +2. Navigate to module page +3. Check for errors: + - RPC errors (object not found, method not found, access denied) + - JavaScript errors (api.method is not a function) + - 404 errors (view files not found) +4. Test functionality: + - Load data displays correctly + - Actions work (start/stop services, save settings) + - No "NaN", "undefined", or empty values + +### Remote ubus Testing + +Test RPCD methods on router: + +```bash +# List all methods +ssh router "ubus list luci.system-hub" + +# Call method without parameters +ssh router "ubus call luci.system-hub status" + +# Call method with parameters +ssh router "ubus call luci.system-hub service_action '{\"service\":\"network\",\"action\":\"restart\"}'" + +# Pretty-print JSON output +ssh router "ubus call luci.system-hub get_health | jsonlint" +``` + +### Debugging Tips + +**Enable RPCD debug logging**: +```bash +# Edit /etc/init.d/rpcd +# Add -v flag to procd_set_param command +procd_set_param command "$PROG" -v + +# Restart RPCD +/etc/init.d/rpcd restart + +# Watch logs +logread -f | grep rpcd +``` + +**Enable JavaScript console logging**: +```javascript +// Add to api.js +console.log('🔧 API v0.1.0 loaded at', new Date().toISOString()); + +// Add to views +console.log('Loading health data...'); +API.getHealth().then(function(data) { + console.log('Health data:', data); +}); +``` + +**Test JSON output**: +```bash +# On router +/usr/libexec/rpcd/luci.system-hub call get_health | jsonlint + +# Check for common errors +# - Missing commas +# - Trailing commas +# - Unquoted keys +# - Invalid escape sequences +``` + +--- + +## Best Practices Summary + +### DO: +✅ Use `luci.` prefix for all ubus objects +✅ Name RPCD scripts to match ubus object exactly +✅ Use `baseclass.extend()` for API modules +✅ Import APIs with `'require module/api as API'` pattern +✅ Add null/undefined checks in frontend: `health.cpu?.usage || 0` +✅ Validate JSON with `jsonlint` before deploying +✅ Test with `ubus call` before browser testing +✅ Restart RPCD after backend changes +✅ Clear browser cache after frontend changes +✅ Run `./secubox-tools/validate-modules.sh` before committing + +### DON'T: +❌ Use ubus object names without `luci.` prefix +❌ Use `baseclass.singleton()` or plain objects for API modules +❌ Import APIs with `L.require('module.path')` (returns class, not instance) +❌ Forget to add methods to ACL file +❌ Mix up read/write methods in ACL sections +❌ Output non-JSON from RPCD scripts +❌ Use inconsistent data structures between backend and frontend +❌ Deploy without testing locally first +❌ Assume data exists - always check for null/undefined +❌ Forget to make RPCD scripts executable (`chmod +x`) + +--- + +## Version History + +**v1.0** (2025-12-26) +- Initial reference guide +- Based on luci-app-secubox v1.0.0 and luci-app-system-hub v0.1.0 +- Documented all critical patterns and common errors +- Validated against real-world implementation challenges + +--- + +## References + +- **OpenWrt Documentation**: https://openwrt.org/docs/guide-developer/start +- **LuCI Documentation**: https://github.com/openwrt/luci/wiki +- **ubus Documentation**: https://openwrt.org/docs/techref/ubus +- **UCI Documentation**: https://openwrt.org/docs/guide-user/base-system/uci +- **jshn.sh Library**: `/usr/share/libubox/jshn.sh` on OpenWrt + +--- + +## Contact + +For questions or contributions to this reference guide: +- **Author**: CyberMind +- **Project**: SecuBox OpenWrt +- **Repository**: https://github.com/cybermind-fr/secubox-openwrt + +--- + +**END OF REFERENCE GUIDE** diff --git a/luci-app-secubox/Makefile b/luci-app-secubox/Makefile index b08a15e..3944699 100644 --- a/luci-app-secubox/Makefile +++ b/luci-app-secubox/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-secubox -PKG_VERSION:=0.1.0 +PKG_VERSION:=0.1.1 PKG_RELEASE:=1 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=CyberMind diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/alerts.css b/luci-app-secubox/htdocs/luci-static/resources/secubox/alerts.css index 52aeedf..6ee194e 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/secubox/alerts.css +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/alerts.css @@ -40,7 +40,7 @@ /* Controls */ .secubox-alerts-controls { - background: white; + background: var(--sb-bg-card); border-radius: 12px; padding: 20px; margin-bottom: 24px; @@ -61,7 +61,7 @@ .secubox-filter-group label { font-weight: 600; font-size: 13px; - color: #64748b; + color: var(--sb-text-muted); text-transform: uppercase; letter-spacing: 0.5px; } @@ -75,7 +75,7 @@ } .secubox-alert-stat-card { - background: white; + background: var(--sb-bg-card); border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); @@ -114,7 +114,7 @@ /* Alert Item */ .secubox-alert-item { - background: white; + background: var(--sb-bg-card); border-radius: 8px; padding: 16px; display: flex; @@ -170,12 +170,12 @@ .secubox-alert-module { font-size: 16px; - color: #1e293b; + color: var(--sb-text); } .secubox-alert-time { font-size: 12px; - color: #64748b; + color: var(--sb-text-muted); } .secubox-alert-message { @@ -225,7 +225,7 @@ border-radius: 50%; border: none; background: rgba(0, 0, 0, 0.05); - color: #64748b; + color: var(--sb-text-muted); cursor: pointer; font-size: 18px; line-height: 1; @@ -256,7 +256,7 @@ .secubox-empty-title { font-size: 20px; font-weight: 600; - color: #64748b; + color: var(--sb-text-muted); margin-bottom: 8px; } diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js b/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js index 8b1e4d2..ab30171 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js @@ -89,6 +89,12 @@ var callDashboardData = rpc.declare({ expect: { } }); +var callGetTheme = rpc.declare({ + object: 'luci.secubox', + method: 'get_theme', + expect: { } +}); + function formatUptime(seconds) { if (!seconds) return '0s'; var d = Math.floor(seconds / 86400); @@ -121,6 +127,7 @@ return baseclass.extend({ getAlerts: callAlerts, quickAction: callQuickAction, getDashboardData: callDashboardData, + getTheme: callGetTheme, formatUptime: formatUptime, formatBytes: formatBytes }); diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/dashboard.css b/luci-app-secubox/htdocs/luci-static/resources/secubox/dashboard.css index 60ac41c..ae78a0e 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/secubox/dashboard.css +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/dashboard.css @@ -66,10 +66,11 @@ } .secubox-stat-card { - background: white; + background: var(--sb-bg-card); + border: 1px solid var(--sb-border); border-radius: 12px; padding: 20px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + box-shadow: 0 2px 8px var(--sb-shadow); display: flex; align-items: center; gap: 16px; @@ -78,7 +79,7 @@ .secubox-stat-card:hover { transform: translateY(-4px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + box-shadow: 0 4px 12px var(--sb-hover-shadow); } .secubox-stat-icon { @@ -95,11 +96,12 @@ font-weight: 700; line-height: 1; margin-bottom: 4px; + color: var(--sb-text); } .secubox-stat-label { font-size: 13px; - color: #64748b; + color: var(--sb-text-muted); text-transform: uppercase; letter-spacing: 0.5px; } @@ -126,18 +128,20 @@ /* Card */ .secubox-card { - background: white; + background: var(--sb-bg-card); + border: 1px solid var(--sb-border); border-radius: 12px; padding: 24px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + box-shadow: 0 2px 8px var(--sb-shadow); } .secubox-card-title { margin: 0 0 20px 0; font-size: 18px; font-weight: 600; - color: #1e293b; - border-bottom: 2px solid #f1f5f9; + color: var(--sb-text); + color: var(--sb-text); + border-bottom: 2px solid var(--sb-border); padding-bottom: 12px; } @@ -188,7 +192,7 @@ .secubox-gauge-label { font-size: 12px; - color: #64748b; + color: var(--sb-text-muted); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; @@ -196,7 +200,7 @@ .secubox-gauge-details { font-size: 12px; - color: #64748b; + color: var(--sb-text-muted); text-align: center; max-width: 140px; } @@ -220,7 +224,7 @@ } .secubox-module-mini-card { - background: #f8fafc; + background: var(--sb-bg); border-radius: 8px; padding: 12px; transition: all 0.2s; @@ -228,7 +232,7 @@ } .secubox-module-link:hover .secubox-module-mini-card { - background: #f1f5f9; + background: var(--sb-bg); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } @@ -274,13 +278,13 @@ .secubox-module-mini-name { font-weight: 600; font-size: 13px; - color: #1e293b; + color: var(--sb-text); line-height: 1.3; } .secubox-module-mini-status { font-size: 11px; - color: #64748b; + color: var(--sb-text-muted); } .secubox-module-running .secubox-module-mini-status { @@ -295,8 +299,8 @@ } .secubox-action-btn { - background: white; - border: 2px solid #e2e8f0; + background: var(--sb-bg-card); + border: 2px solid var(--sb-border); border-radius: 8px; padding: 12px; cursor: pointer; @@ -320,7 +324,7 @@ } .secubox-action-label { - color: #1e293b; + color: var(--sb-text); } /* Alerts */ diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/monitoring.css b/luci-app-secubox/htdocs/luci-static/resources/secubox/monitoring.css index 2b98587..dfa7dcc 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/secubox/monitoring.css +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/monitoring.css @@ -26,7 +26,7 @@ /* Chart Card */ .secubox-chart-card { - background: white; + background: var(--sb-bg-card); border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); @@ -42,7 +42,7 @@ margin: 0 0 16px 0; font-size: 16px; font-weight: 600; - color: #1e293b; + color: var(--sb-text); } .secubox-chart-container { @@ -50,7 +50,7 @@ height: 200px; position: relative; margin-bottom: 16px; - background: #f8fafc; + background: var(--sb-bg); border-radius: 8px; overflow: hidden; } @@ -76,7 +76,7 @@ .secubox-chart-unit { font-size: 12px; - color: #64748b; + color: var(--sb-text-muted); text-transform: uppercase; letter-spacing: 0.5px; } @@ -93,7 +93,7 @@ align-items: center; gap: 16px; padding: 16px; - background: #f8fafc; + background: var(--sb-bg); border-radius: 8px; transition: background 0.2s; } @@ -113,14 +113,14 @@ .secubox-stat-label { font-size: 13px; - color: #64748b; + color: var(--sb-text-muted); margin-bottom: 4px; } .secubox-stat-value { font-size: 20px; font-weight: 600; - color: #1e293b; + color: var(--sb-text); } /* Animations */ diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/secubox.css b/luci-app-secubox/htdocs/luci-static/resources/secubox/secubox.css index 3e1f693..1af623c 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/secubox/secubox.css +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/secubox.css @@ -2,19 +2,39 @@ * SecuBox Dashboard Styles * Copyright (C) 2025 CyberMind.fr * SPDX-License-Identifier: Apache-2.0 + * Theme-aware styles with dark/light mode support */ +/* Common colors (theme-independent) */ :root { --sb-primary: #3b82f6; --sb-primary-dark: #1e40af; --sb-success: #22c55e; --sb-warning: #f59e0b; --sb-danger: #ef4444; +} + +/* Dark theme (default) */ +:root, +[data-theme="dark"] { --sb-bg: #0f172a; --sb-bg-card: #1e293b; --sb-border: #334155; --sb-text: #f1f5f9; --sb-text-muted: #94a3b8; + --sb-shadow: rgba(0, 0, 0, 0.3); + --sb-hover-shadow: rgba(0, 0, 0, 0.4); +} + +/* Light theme */ +[data-theme="light"] { + --sb-bg: #f8fafc; + --sb-bg-card: #ffffff; + --sb-border: #e2e8f0; + --sb-text: #0f172a; + --sb-text-muted: #64748b; + --sb-shadow: rgba(0, 0, 0, 0.08); + --sb-hover-shadow: rgba(0, 0, 0, 0.12); } .secubox-dashboard { @@ -42,7 +62,7 @@ .secubox-stat-box:hover { transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + box-shadow: 0 4px 12px var(--sb-shadow); } .secubox-stat-icon { @@ -236,7 +256,7 @@ .secubox-module-card:hover { transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + box-shadow: 0 4px 12px var(--sb-hover-shadow); border-color: var(--sb-primary); } diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/theme.js b/luci-app-secubox/htdocs/luci-static/resources/secubox/theme.js new file mode 100644 index 0000000..36f8cf7 --- /dev/null +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/theme.js @@ -0,0 +1,63 @@ +'use strict'; +'require baseclass'; +'require secubox/api as API'; + +/** + * SecuBox Theme Manager + * Manages dark/light/system theme switching across SecuBox and all modules + * Version: 1.0.0 + */ + +console.log('🎨 SecuBox Theme Manager v1.0.0 loaded'); + +return baseclass.extend({ + /** + * Initialize theme system + * Loads theme preference and applies it to the page + */ + init: function() { + var self = this; + + return API.getTheme().then(function(data) { + var themePref = data.theme || 'dark'; + self.applyTheme(themePref); + + // Listen for system theme changes if preference is 'system' + if (themePref === 'system' && window.matchMedia) { + var darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); + darkModeQuery.addListener(function() { + self.applyTheme('system'); + }); + } + }).catch(function(err) { + console.error('Failed to load theme preference, using dark theme:', err); + self.applyTheme('dark'); + }); + }, + + /** + * Apply theme to the page + * @param {string} theme - Theme preference: 'dark', 'light', or 'system' + */ + applyTheme: function(theme) { + var effectiveTheme = theme; + + // If 'system', detect from OS + if (theme === 'system' && window.matchMedia) { + effectiveTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + + // Apply theme to document root + document.documentElement.setAttribute('data-theme', effectiveTheme); + + console.log('🎨 Theme applied:', theme, '(effective:', effectiveTheme + ')'); + }, + + /** + * Get current effective theme + * @returns {string} 'dark' or 'light' + */ + getCurrentTheme: function() { + return document.documentElement.getAttribute('data-theme') || 'dark'; + } +}); diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/alerts.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/alerts.js index a1c2be0..d46df52 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/alerts.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/alerts.js @@ -3,15 +3,24 @@ 'require ui'; 'require dom'; 'require secubox/api as API'; +'require secubox/theme as Theme'; 'require poll'; -// Load CSS +// Load CSS (base theme variables first) +document.head.appendChild(E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('secubox/secubox.css') +})); document.head.appendChild(E('link', { 'rel': 'stylesheet', 'type': 'text/css', 'href': L.resource('secubox/alerts.css') })); +// Initialize theme +Theme.init(); + return view.extend({ alertsData: null, filterSeverity: 'all', diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js index 131027f..f3acdb2 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js @@ -2,16 +2,25 @@ 'require view'; 'require ui'; 'require secubox/api as API'; +'require secubox/theme as Theme'; 'require dom'; 'require poll'; -// Load CSS +// Load CSS (base theme variables first) +document.head.appendChild(E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('secubox/secubox.css') +})); document.head.appendChild(E('link', { 'rel': 'stylesheet', 'type': 'text/css', 'href': L.resource('secubox/dashboard.css') })); +// Initialize theme +Theme.init(); + return view.extend({ dashboardData: null, healthData: null, diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules-debug.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules-debug.js index e765f14..0210b67 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules-debug.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules-debug.js @@ -1,6 +1,10 @@ 'use strict'; 'require view'; 'require rpc'; +'require secubox/theme as Theme'; + +// Initialize theme +Theme.init(); var callModules = rpc.declare({ object: 'luci.secubox', diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules-minimal.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules-minimal.js index bb3810e..a54f15d 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules-minimal.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules-minimal.js @@ -1,5 +1,9 @@ 'use strict'; 'require view'; +'require secubox/theme as Theme'; + +// Initialize theme +Theme.init(); return view.extend({ load: function() { diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js index 36a6b35..8b9f9ef 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js @@ -1,6 +1,10 @@ 'use strict'; 'require view'; 'require rpc'; +'require secubox/theme as Theme'; + +// Initialize theme +Theme.init(); var callModules = rpc.declare({ object: 'luci.secubox', diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js index 24be019..9d6bb11 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js @@ -3,15 +3,24 @@ 'require ui'; 'require dom'; 'require secubox/api as API'; +'require secubox/theme as Theme'; 'require poll'; -// Load CSS +// Load CSS (base theme variables first) +document.head.appendChild(E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('secubox/secubox.css') +})); document.head.appendChild(E('link', { 'rel': 'stylesheet', 'type': 'text/css', 'href': L.resource('secubox/monitoring.css') })); +// Initialize theme +Theme.init(); + return view.extend({ cpuHistory: [], memoryHistory: [], diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/settings.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/settings.js index ad8469c..4fce7a8 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/settings.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/settings.js @@ -4,6 +4,10 @@ 'require uci'; 'require ui'; 'require secubox/api as API'; +'require secubox/theme as Theme'; + +// Initialize theme +Theme.init(); return view.extend({ load: function() { diff --git a/luci-app-secubox/root/usr/libexec/rpcd/luci.secubox b/luci-app-secubox/root/usr/libexec/rpcd/luci.secubox index 991a043..a46278e 100755 --- a/luci-app-secubox/root/usr/libexec/rpcd/luci.secubox +++ b/luci-app-secubox/root/usr/libexec/rpcd/luci.secubox @@ -86,7 +86,7 @@ get_status() { local running=0 json_init - json_add_string "version" "0.1.0" + json_add_string "version" "0.1.1" json_add_string "hostname" "$(uci -q get system.@system[0].hostname || echo 'SecuBox')" # System info @@ -590,7 +590,7 @@ get_dashboard_data() { local mem_pct=$((mem_used * 100 / mem_total)) json_add_object "status" - json_add_string "version" "0.1.0" + json_add_string "version" "0.1.1" json_add_string "hostname" "$(uci -q get system.@system[0].hostname || echo 'SecuBox')" json_add_int "uptime" "$uptime" json_add_string "load" "$load" @@ -667,6 +667,21 @@ get_dashboard_data() { json_dump } +# Get theme setting +get_theme() { + local theme="dark" + + # Load secubox config + if [ -f "/etc/config/secubox" ]; then + config_load secubox + config_get theme main theme "dark" + fi + + json_init + json_add_string "theme" "$theme" + json_dump +} + # Main dispatcher case "$1" in list) @@ -703,6 +718,8 @@ case "$1" in json_close_object json_add_object "get_dashboard_data" json_close_object + json_add_object "get_theme" + json_close_object json_dump ;; call) @@ -764,6 +781,9 @@ case "$1" in get_dashboard_data) get_dashboard_data ;; + get_theme) + get_theme + ;; *) echo '{"error":"Unknown method"}' ;; diff --git a/luci-app-secubox/root/usr/share/rpcd/acl.d/luci-app-secubox.json b/luci-app-secubox/root/usr/share/rpcd/acl.d/luci-app-secubox.json index a2f863c..b49e02f 100644 --- a/luci-app-secubox/root/usr/share/rpcd/acl.d/luci-app-secubox.json +++ b/luci-app-secubox/root/usr/share/rpcd/acl.d/luci-app-secubox.json @@ -12,7 +12,8 @@ "diagnostics", "get_system_health", "get_alerts", - "get_dashboard_data" + "get_dashboard_data", + "get_theme" ], "uci": [ "get", diff --git a/luci-app-system-hub/Makefile b/luci-app-system-hub/Makefile index 89c6829..a83e860 100644 --- a/luci-app-system-hub/Makefile +++ b/luci-app-system-hub/Makefile @@ -1,10 +1,10 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-system-hub -PKG_VERSION:=0.0.2 +PKG_VERSION:=0.1.1 PKG_RELEASE:=1 PKG_LICENSE:=Apache-2.0 -PKG_MAINTAINER:=SecuBox Project +PKG_MAINTAINER:=CyberMind LUCI_TITLE:=System Hub - Central Control Dashboard LUCI_DESCRIPTION:=Central system control with monitoring, services, logs, and backup diff --git a/luci-app-system-hub/README.md b/luci-app-system-hub/README.md index ca2cf6e..279a032 100644 --- a/luci-app-system-hub/README.md +++ b/luci-app-system-hub/README.md @@ -124,14 +124,13 @@ Output: ```json { "cpu": { - "model": "ARM Cortex-A72", + "usage": 25, + "status": "ok", + "load_1m": "0.25", + "load_5m": "0.30", + "load_15m": "0.28", "cores": 4 }, - "load": { - "1min": "0.25", - "5min": "0.30", - "15min": "0.28" - }, "memory": { "total_kb": 4096000, "free_kb": 2048000, @@ -139,23 +138,31 @@ Output: "used_kb": 1024000, "buffers_kb": 512000, "cached_kb": 1536000, - "percent": 25 + "usage": 25, + "status": "ok" }, - "storage": [ - { - "filesystem": "/dev/mmcblk0p2", - "size": "29G", - "used": "5.2G", - "available": "22G", - "percent": 19, - "mountpoint": "/" - } - ], - "temperatures": [ - { - "zone": "thermal_zone0", - "celsius": 45 - } + "disk": { + "total_kb": 30408704, + "used_kb": 5447680, + "usage": 19, + "status": "ok" + }, + "temperature": { + "value": 45, + "status": "ok" + }, + "network": { + "wan_up": true, + "status": "ok" + }, + "services": { + "running": 35, + "failed": 2 + }, + "score": 92, + "timestamp": "2025-12-26 10:30:00", + "recommendations": [ + "2 service(s) enabled but not running. Check service status." ] } ``` @@ -227,6 +234,70 @@ System will reboot after 3 seconds. ubus call luci.system-hub get_storage ``` +#### Get Settings + +```bash +ubus call luci.system-hub get_settings +``` + +Output: +```json +{ + "general": { + "auto_refresh": true, + "health_check": true, + "debug_mode": false, + "refresh_interval": 30, + "log_retention": 30 + }, + "thresholds": { + "cpu_warning": 80, + "cpu_critical": 95, + "mem_warning": 80, + "mem_critical": 95, + "disk_warning": 80, + "disk_critical": 95, + "temp_warning": 70, + "temp_critical": 85 + }, + "schedules": { + "health_report": true, + "backup_weekly": true, + "log_cleanup": true + }, + "upload": { + "auto_upload": false, + "url": "", + "token": "" + }, + "support": { + "provider": "CyberMind.fr", + "email": "support@cybermind.fr", + "docs": "https://docs.cybermind.fr" + } +} +``` + +#### Save Settings + +```bash +ubus call luci.system-hub save_settings '{ + "auto_refresh": 1, + "health_check": 1, + "debug_mode": 0, + "refresh_interval": 30, + "log_retention": 30, + "cpu_warning": 80, + "cpu_critical": 95, + "mem_warning": 80, + "mem_critical": 95, + "disk_warning": 80, + "disk_critical": 95, + "temp_warning": 70, + "temp_critical": 85 +}' +``` + ## ubus API Reference ### status() @@ -359,6 +430,75 @@ Reboot the system (3-second delay). Get detailed storage information for all mount points. +### get_settings() + +Get all system-hub configuration settings. + +**Returns:** +```json +{ + "general": { + "auto_refresh": true, + "health_check": true, + "debug_mode": false, + "refresh_interval": 30, + "log_retention": 30 + }, + "thresholds": { + "cpu_warning": 80, + "cpu_critical": 95, + "mem_warning": 80, + "mem_critical": 95, + "disk_warning": 80, + "disk_critical": 95, + "temp_warning": 70, + "temp_critical": 85 + }, + "schedules": { + "health_report": true, + "backup_weekly": true, + "log_cleanup": true + }, + "upload": { + "auto_upload": false, + "url": "", + "token": "" + }, + "support": { + "provider": "CyberMind.fr", + "email": "support@cybermind.fr", + "docs": "https://docs.cybermind.fr" + } +} +``` + +### save_settings(...) + +Save system-hub configuration settings to UCI. + +**Parameters:** +- `auto_refresh`: Enable auto-refresh (0|1) +- `health_check`: Enable automatic health checks (0|1) +- `debug_mode`: Enable debug mode (0|1) +- `refresh_interval`: Refresh interval in seconds +- `log_retention`: Log retention in days +- `cpu_warning`: CPU warning threshold (%) +- `cpu_critical`: CPU critical threshold (%) +- `mem_warning`: Memory warning threshold (%) +- `mem_critical`: Memory critical threshold (%) +- `disk_warning`: Disk warning threshold (%) +- `disk_critical`: Disk critical threshold (%) +- `temp_warning`: Temperature warning threshold (°C) +- `temp_critical`: Temperature critical threshold (°C) + +**Returns:** +```json +{ + "success": true, + "message": "Settings saved successfully" +} +``` + ## System Information Sources - Hostname: `/proc/sys/kernel/hostname` @@ -438,8 +578,37 @@ Apache-2.0 ## Maintainer -SecuBox Project +CyberMind ## Version -0.0.2 +0.1.0 + +## Changelog + +### v0.1.0 (2025-12-26) +- **STABLE RELEASE** - Production ready +- Fixed overview.js: Updated to use new health data structure (cpu.usage, memory.usage, disk.usage instead of deprecated fields) +- Fixed health view: Complete restructure of get_health RPCD method with proper metrics + - CPU: usage %, status (ok/warning/critical), load averages, cores count + - Memory: usage %, status, detailed KB metrics + - Disk: root filesystem usage %, status, size metrics + - Temperature: system temperature with status + - Network: WAN connectivity check + - Services: running vs failed count + - Overall health score: 0-100 based on all metrics + - Dynamic recommendations: actionable alerts based on thresholds +- Fixed settings view: Complete implementation with UCI backend + - Added get_settings and save_settings RPCD methods + - General settings: auto-refresh, health check, debug mode, intervals + - Alert thresholds: configurable CPU, memory, disk, temperature limits + - Scheduled tasks configuration + - Upload and support information +- Fixed ACL permissions: Added get_settings (read) and save_settings (write) to ACL +- Fixed API module: Correct usage of baseclass.extend() pattern +- Fixed view imports: Use 'require system-hub/api as API' instead of L.require() +- All 12 RPC methods working correctly +- Comprehensive validation passing + +### v0.0.2 +- Initial implementation with basic system monitoring and service management diff --git a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js index ceab590..b8f7604 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js @@ -6,11 +6,11 @@ * System Hub API * Package: luci-app-system-hub * RPCD object: luci.system-hub - * Version: 0.0.2-debug + * Version: 0.1.1 */ // Debug log to verify correct version is loaded -console.log('🔧 System Hub API v0.0.2-debug loaded at', new Date().toISOString()); +console.log('🔧 System Hub API v0.1.1 loaded at', new Date().toISOString()); var callStatus = rpc.declare({ object: 'luci.system-hub', @@ -75,6 +75,19 @@ var callGetStorage = rpc.declare({ expect: { storage: [] } }); +var callGetSettings = rpc.declare({ + object: 'luci.system-hub', + method: 'get_settings', + expect: {} +}); + +var callSaveSettings = rpc.declare({ + object: 'luci.system-hub', + method: 'save_settings', + params: ['auto_refresh', 'health_check', 'debug_mode', 'refresh_interval', 'log_retention', 'cpu_warning', 'cpu_critical', 'mem_warning', 'mem_critical', 'disk_warning', 'disk_critical', 'temp_warning', 'temp_critical'], + expect: {} +}); + return baseclass.extend({ // RPC methods - exposed via ubus getStatus: callStatus, @@ -86,5 +99,7 @@ return baseclass.extend({ backupConfig: callBackupConfig, restoreConfig: callRestoreConfig, reboot: callReboot, - getStorage: callGetStorage + getStorage: callGetStorage, + getSettings: callGetSettings, + saveSettings: callSaveSettings }); diff --git a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/dashboard.css b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/dashboard.css index 21d7ac0..c33045c 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/dashboard.css +++ b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/dashboard.css @@ -1,17 +1,9 @@ /* System Hub Dashboard - Central Control Theme */ /* Copyright (C) 2024 CyberMind.fr - Gandalf */ +/* Theme-aware styles with dark/light mode support */ +/* Common variables (theme-independent) */ :root { - --sh-bg-primary: #0a0a0f; - --sh-bg-secondary: #12121a; - --sh-bg-tertiary: #1a1a24; - --sh-border: #2a2a3a; - --sh-border-light: #3a3a4a; - - --sh-text-primary: #fafafa; - --sh-text-secondary: #a0a0b0; - --sh-text-muted: #707080; - --sh-accent-indigo: #6366f1; --sh-accent-violet: #8b5cf6; --sh-accent-blue: #3b82f6; @@ -19,24 +11,55 @@ --sh-accent-green: #22c55e; --sh-accent-amber: #f59e0b; --sh-accent-red: #ef4444; - + --sh-success: #22c55e; --sh-warning: #f59e0b; --sh-danger: #ef4444; --sh-info: #3b82f6; - + --sh-gradient: linear-gradient(135deg, #6366f1, #8b5cf6, #a855f7); --sh-gradient-soft: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.1)); - + --sh-font-mono: 'JetBrains Mono', 'Fira Code', monospace; --sh-font-sans: 'Inter', -apple-system, sans-serif; - + --sh-radius: 8px; --sh-radius-lg: 12px; +} + +/* Dark theme (default) */ +:root, +[data-theme="dark"] { + --sh-bg-primary: #0a0a0f; + --sh-bg-secondary: #12121a; + --sh-bg-tertiary: #1a1a24; + --sh-border: #2a2a3a; + --sh-border-light: #3a3a4a; + + --sh-text-primary: #fafafa; + --sh-text-secondary: #a0a0b0; + --sh-text-muted: #707080; + --sh-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); --sh-shadow-glow: 0 0 30px rgba(99, 102, 241, 0.3); } +/* Light theme */ +[data-theme="light"] { + --sh-bg-primary: #f5f5f7; + --sh-bg-secondary: #ffffff; + --sh-bg-tertiary: #f9fafb; + --sh-border: #e5e7eb; + --sh-border-light: #d1d5db; + + --sh-text-primary: #0a0a0f; + --sh-text-secondary: #4b5563; + --sh-text-muted: #9ca3af; + + --sh-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + --sh-shadow-glow: 0 0 30px rgba(99, 102, 241, 0.2); +} + /* Base */ .system-hub-dashboard { font-family: var(--sh-font-sans); diff --git a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/theme.js b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/theme.js new file mode 100644 index 0000000..f0ce762 --- /dev/null +++ b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/theme.js @@ -0,0 +1,70 @@ +'use strict'; +'require baseclass'; +'require rpc'; + +/** + * System Hub Theme Manager + * Uses centralized theme from SecuBox configuration + * Version: 1.0.0 + */ + +console.log('🎨 System Hub Theme Manager v1.0.0 loaded'); + +// RPC call to get theme from secu-box config +var callGetTheme = rpc.declare({ + object: 'luci.secubox', + method: 'get_theme', + expect: {} +}); + +return baseclass.extend({ + /** + * Initialize theme system + * Loads theme preference from SecuBox and applies it to the page + */ + init: function() { + var self = this; + + return callGetTheme().then(function(data) { + var themePref = data.theme || 'dark'; + self.applyTheme(themePref); + + // Listen for system theme changes if preference is 'system' + if (themePref === 'system' && window.matchMedia) { + var darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); + darkModeQuery.addListener(function() { + self.applyTheme('system'); + }); + } + }).catch(function(err) { + console.error('Failed to load theme preference from SecuBox, using dark theme:', err); + self.applyTheme('dark'); + }); + }, + + /** + * Apply theme to the page + * @param {string} theme - Theme preference: 'dark', 'light', or 'system' + */ + applyTheme: function(theme) { + var effectiveTheme = theme; + + // If 'system', detect from OS + if (theme === 'system' && window.matchMedia) { + effectiveTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + + // Apply theme to document root + document.documentElement.setAttribute('data-theme', effectiveTheme); + + console.log('🎨 System Hub theme applied:', theme, '(effective:', effectiveTheme + ')'); + }, + + /** + * Get current effective theme + * @returns {string} 'dark' or 'light' + */ + getCurrentTheme: function() { + return document.documentElement.getAttribute('data-theme') || 'dark'; + } +}); diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js index 1f51fc6..68bba6e 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js @@ -2,6 +2,17 @@ 'require view'; 'require ui'; 'require system-hub/api as API'; +'require system-hub/theme as Theme'; + +// Load CSS +document.head.appendChild(E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('system-hub/dashboard.css') +})); + +// Initialize theme +Theme.init(); return L.view.extend({ load: function() { diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js index 96396a6..340d79d 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js @@ -2,8 +2,18 @@ 'require view'; 'require dom'; 'require ui'; +'require system-hub/api as API'; +'require system-hub/theme as Theme'; -var api = L.require('system-hub.api'); +// Load CSS +document.head.appendChild(E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('system-hub/dashboard.css') +})); + +// Initialize theme +Theme.init(); // Helper: Get health status info based on score function getHealthStatus(score) { @@ -24,7 +34,7 @@ function formatBytes(bytes) { return view.extend({ load: function() { - return api.getHealth(); + return API.getHealth(); }, render: function(data) { diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js index 9077540..723e906 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js @@ -2,6 +2,17 @@ 'require view'; 'require ui'; 'require system-hub/api as API'; +'require system-hub/theme as Theme'; + +// Load CSS +document.head.appendChild(E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('system-hub/dashboard.css') +})); + +// Initialize theme +Theme.init(); return L.view.extend({ load: function() { diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js index 82d2d8e..428acd5 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js @@ -2,6 +2,17 @@ 'require view'; 'require poll'; 'require system-hub/api as API'; +'require system-hub/theme as Theme'; + +// Load CSS +document.head.appendChild(E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('system-hub/dashboard.css') +})); + +// Initialize theme +Theme.init(); return L.view.extend({ load: function() { @@ -68,27 +79,22 @@ return L.view.extend({ var gaugesContainer = E('div', { 'style': 'display: flex; justify-content: space-around; flex-wrap: wrap; margin: 20px 0;' }); // CPU Load Gauge - var cpuLoad = parseFloat(health.load ? health.load['1min'] : status.health ? status.health.cpu_load : '0'); - var cpuPercent = Math.min((cpuLoad * 100 / (health.cpu ? health.cpu.cores : 1)), 100); + var cpuLoad = parseFloat(health.cpu ? health.cpu.load_1m : '0'); + var cpuPercent = health.cpu ? health.cpu.usage : 0; gaugesContainer.appendChild(this.createGauge('CPU Load', cpuPercent, cpuLoad.toFixed(2))); // Memory Gauge - var memPercent = health.memory ? health.memory.percent : (status.health ? status.health.mem_percent : 0); + var memPercent = health.memory ? health.memory.usage : 0; var memUsed = health.memory ? (health.memory.used_kb / 1024).toFixed(0) : 0; var memTotal = health.memory ? (health.memory.total_kb / 1024).toFixed(0) : 0; gaugesContainer.appendChild(this.createGauge('Memory', memPercent, memUsed + ' / ' + memTotal + ' MB')); // Disk Gauge - var diskPercent = status.disk_percent || 0; - var diskInfo = ''; - if (health.storage && health.storage.length > 0) { - var root = health.storage.find(function(s) { return s.mountpoint === '/'; }); - if (root) { - diskPercent = root.percent; - diskInfo = root.used + ' / ' + root.size; - } - } - gaugesContainer.appendChild(this.createGauge('Disk Usage', diskPercent, diskInfo || diskPercent + '%')); + var diskPercent = health.disk ? health.disk.usage : 0; + var diskUsed = health.disk ? (health.disk.used_kb / 1024).toFixed(0) : 0; + var diskTotal = health.disk ? (health.disk.total_kb / 1024).toFixed(0) : 0; + var diskInfo = diskUsed + ' / ' + diskTotal + ' MB'; + gaugesContainer.appendChild(this.createGauge('Disk Usage', diskPercent, diskInfo)); healthSection.appendChild(gaugesContainer); v.appendChild(healthSection); @@ -99,19 +105,19 @@ return L.view.extend({ E('h3', {}, _('CPU Information')), E('div', { 'class': 'table' }, [ E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left', 'width': '50%' }, [ - E('strong', {}, _('Model: ')), - E('span', {}, health.cpu.model) - ]), E('div', { 'class': 'td left', 'width': '50%' }, [ E('strong', {}, _('Cores: ')), E('span', {}, String(health.cpu.cores)) + ]), + E('div', { 'class': 'td left', 'width': '50%' }, [ + E('strong', {}, _('Usage: ')), + E('span', {}, health.cpu.usage + '%') ]) ]), E('div', { 'class': 'tr' }, [ E('div', { 'class': 'td left' }, [ E('strong', {}, _('Load Average: ')), - E('span', {}, (health.load ? health.load['1min'] + ' / ' + health.load['5min'] + ' / ' + health.load['15min'] : 'N/A')) + E('span', {}, (health.cpu.load_1m + ' / ' + health.cpu.load_5m + ' / ' + health.cpu.load_15m)) ]) ]) ]) @@ -120,71 +126,54 @@ return L.view.extend({ } // Temperature - if (health.temperatures && health.temperatures.length > 0) { + if (health.temperature && health.temperature.value > 0) { + var tempValue = health.temperature.value; + var tempColor = tempValue > 80 ? 'red' : (tempValue > 60 ? 'orange' : 'green'); var tempSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Temperature')) - ]); - - var tempTable = E('table', { 'class': 'table' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th' }, _('Zone')), - E('th', { 'class': 'th' }, _('Temperature')) + E('h3', {}, _('Temperature')), + E('div', { 'class': 'table' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td left' }, [ + E('strong', {}, _('System Temperature: ')), + E('span', { 'style': 'color: ' + tempColor + '; font-weight: bold;' }, tempValue + '°C') + ]) + ]) ]) ]); - - health.temperatures.forEach(function(temp) { - var color = temp.celsius > 80 ? 'red' : (temp.celsius > 60 ? 'orange' : 'green'); - tempTable.appendChild(E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, temp.zone), - E('td', { 'class': 'td' }, [ - E('span', { 'style': 'color: ' + color + '; font-weight: bold;' }, temp.celsius + '°C') - ]) - ])); - }); - - tempSection.appendChild(tempTable); v.appendChild(tempSection); } - // Storage - if (health.storage && health.storage.length > 0) { + // Storage (Root Filesystem) + if (health.disk) { + var diskColor = health.disk.usage > 90 ? 'red' : (health.disk.usage > 75 ? 'orange' : 'green'); var storageSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Storage')) - ]); - - var storageTable = E('table', { 'class': 'table' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th' }, _('Mountpoint')), - E('th', { 'class': 'th' }, _('Filesystem')), - E('th', { 'class': 'th' }, _('Size')), - E('th', { 'class': 'th' }, _('Used')), - E('th', { 'class': 'th' }, _('Available')), - E('th', { 'class': 'th' }, _('Use %')) - ]) - ]); - - health.storage.forEach(function(storage) { - var color = storage.percent > 90 ? 'red' : (storage.percent > 75 ? 'orange' : 'green'); - storageTable.appendChild(E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, E('strong', {}, storage.mountpoint)), - E('td', { 'class': 'td' }, E('code', {}, storage.filesystem)), - E('td', { 'class': 'td' }, storage.size), - E('td', { 'class': 'td' }, storage.used), - E('td', { 'class': 'td' }, storage.available), - E('td', { 'class': 'td' }, [ - E('div', { 'style': 'display: flex; align-items: center;' }, [ - E('div', { 'style': 'flex: 1; background: #eee; height: 10px; border-radius: 5px; margin-right: 10px;' }, [ - E('div', { - 'style': 'background: ' + color + '; width: ' + storage.percent + '%; height: 100%; border-radius: 5px;' - }) - ]), - E('span', {}, storage.percent + '%') + E('h3', {}, _('Storage (Root Filesystem)')), + E('div', { 'class': 'table' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td left', 'width': '50%' }, [ + E('strong', {}, _('Total: ')), + E('span', {}, (health.disk.total_kb / 1024).toFixed(0) + ' MB') + ]), + E('div', { 'class': 'td left', 'width': '50%' }, [ + E('strong', {}, _('Used: ')), + E('span', {}, (health.disk.used_kb / 1024).toFixed(0) + ' MB') + ]) + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td left' }, [ + E('strong', {}, _('Usage: ')), + E('div', { 'style': 'display: inline-flex; align-items: center; width: 200px;' }, [ + E('div', { 'style': 'flex: 1; background: #eee; height: 10px; border-radius: 5px; margin-right: 10px;' }, [ + E('div', { + 'style': 'background: ' + diskColor + '; width: ' + health.disk.usage + '%; height: 100%; border-radius: 5px;' + }) + ]), + E('span', { 'style': 'font-weight: bold; color: ' + diskColor }, health.disk.usage + '%') + ]) ]) ]) - ])); - }); - - storageSection.appendChild(storageTable); + ]) + ]); v.appendChild(storageSection); } diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js index 5ec8d77..21d1b99 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js @@ -2,6 +2,17 @@ 'require view'; 'require ui'; 'require system-hub/api as API'; +'require system-hub/theme as Theme'; + +// Load CSS +document.head.appendChild(E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('system-hub/dashboard.css') +})); + +// Initialize theme +Theme.init(); return L.view.extend({ load: function() { diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/settings.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/settings.js index 3cd694d..fc9440b 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/settings.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/settings.js @@ -1,24 +1,40 @@ 'use strict'; 'require view'; 'require dom'; -'require uci'; 'require ui'; +'require system-hub/api as API'; +'require system-hub/theme as Theme'; -var api = L.require('system-hub.api'); +// Load CSS +document.head.appendChild(E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('system-hub/dashboard.css') +})); + +// Initialize theme +Theme.init(); return view.extend({ load: function() { return Promise.all([ - api.getStatus(), - Promise.resolve({ schedules: [] }) // Stub: No schedules yet + API.getSettings(), + API.getStatus() ]); }, render: function(data) { - var status = data[0]; - var schedules = data[1].schedules || []; + var settings = data[0] || {}; + var status = data[1]; var self = this; + // Extract settings with defaults + var general = settings.general || {}; + var thresholds = settings.thresholds || {}; + var schedules = settings.schedules || {}; + var upload = settings.upload || {}; + var support = settings.support || {}; + var view = E('div', { 'class': 'system-hub-dashboard' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), @@ -28,17 +44,17 @@ return view.extend({ E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '⚙️'), 'Configuration Générale' ]) ]), E('div', { 'class': 'sh-card-body' }, [ - this.renderToggle('🔄', 'Rafraîchissement automatique', 'Mettre à jour le dashboard toutes les 30s', true, 'cfg_refresh'), - this.renderToggle('💚', 'Vérification santé auto', 'Exécuter un health check toutes les heures', true, 'cfg_health'), - this.renderToggle('🐛', 'Mode Debug', 'Activer les logs détaillés', false, 'cfg_debug'), - + this.renderToggle('🔄', 'Rafraîchissement automatique', 'Mettre à jour le dashboard toutes les 30s', general.auto_refresh !== false, 'cfg_refresh'), + this.renderToggle('💚', 'Vérification santé auto', 'Exécuter un health check toutes les heures', general.health_check !== false, 'cfg_health'), + this.renderToggle('🐛', 'Mode Debug', 'Activer les logs détaillés', general.debug_mode === true, 'cfg_debug'), + E('div', { 'class': 'sh-form-group', 'style': 'margin-top: 16px;' }, [ E('label', { 'class': 'sh-form-label' }, 'Intervalle de rafraîchissement (secondes)'), - E('input', { 'type': 'number', 'class': 'sh-input', 'value': '30', 'id': 'cfg_interval', 'style': 'width: 120px;' }) + E('input', { 'type': 'number', 'class': 'sh-input', 'value': general.refresh_interval || '30', 'id': 'cfg_interval', 'style': 'width: 120px;' }) ]), E('div', { 'class': 'sh-form-group' }, [ E('label', { 'class': 'sh-form-label' }, 'Rétention des logs (jours)'), - E('input', { 'type': 'number', 'class': 'sh-input', 'value': '30', 'id': 'cfg_retention', 'style': 'width: 120px;' }) + E('input', { 'type': 'number', 'class': 'sh-input', 'value': general.log_retention || '30', 'id': 'cfg_retention', 'style': 'width: 120px;' }) ]) ]) ]), @@ -52,29 +68,29 @@ return view.extend({ E('div', { 'class': 'sh-form-group' }, [ E('label', { 'class': 'sh-form-label' }, 'CPU Warning / Critical (%)'), E('div', { 'style': 'display: flex; gap: 12px;' }, [ - E('input', { 'type': 'number', 'class': 'sh-input', 'value': '80', 'id': 'cpu_warning', 'style': 'width: 100px;' }), - E('input', { 'type': 'number', 'class': 'sh-input', 'value': '95', 'id': 'cpu_critical', 'style': 'width: 100px;' }) + E('input', { 'type': 'number', 'class': 'sh-input', 'value': thresholds.cpu_warning || '80', 'id': 'cpu_warning', 'style': 'width: 100px;' }), + E('input', { 'type': 'number', 'class': 'sh-input', 'value': thresholds.cpu_critical || '95', 'id': 'cpu_critical', 'style': 'width: 100px;' }) ]) ]), E('div', { 'class': 'sh-form-group' }, [ E('label', { 'class': 'sh-form-label' }, 'Mémoire Warning / Critical (%)'), E('div', { 'style': 'display: flex; gap: 12px;' }, [ - E('input', { 'type': 'number', 'class': 'sh-input', 'value': '80', 'id': 'mem_warning', 'style': 'width: 100px;' }), - E('input', { 'type': 'number', 'class': 'sh-input', 'value': '95', 'id': 'mem_critical', 'style': 'width: 100px;' }) + E('input', { 'type': 'number', 'class': 'sh-input', 'value': thresholds.mem_warning || '80', 'id': 'mem_warning', 'style': 'width: 100px;' }), + E('input', { 'type': 'number', 'class': 'sh-input', 'value': thresholds.mem_critical || '95', 'id': 'mem_critical', 'style': 'width: 100px;' }) ]) ]), E('div', { 'class': 'sh-form-group' }, [ E('label', { 'class': 'sh-form-label' }, 'Disque Warning / Critical (%)'), E('div', { 'style': 'display: flex; gap: 12px;' }, [ - E('input', { 'type': 'number', 'class': 'sh-input', 'value': '80', 'id': 'disk_warning', 'style': 'width: 100px;' }), - E('input', { 'type': 'number', 'class': 'sh-input', 'value': '95', 'id': 'disk_critical', 'style': 'width: 100px;' }) + E('input', { 'type': 'number', 'class': 'sh-input', 'value': thresholds.disk_warning || '80', 'id': 'disk_warning', 'style': 'width: 100px;' }), + E('input', { 'type': 'number', 'class': 'sh-input', 'value': thresholds.disk_critical || '95', 'id': 'disk_critical', 'style': 'width: 100px;' }) ]) ]), E('div', { 'class': 'sh-form-group' }, [ E('label', { 'class': 'sh-form-label' }, 'Température Warning / Critical (°C)'), E('div', { 'style': 'display: flex; gap: 12px;' }, [ - E('input', { 'type': 'number', 'class': 'sh-input', 'value': '70', 'id': 'temp_warning', 'style': 'width: 100px;' }), - E('input', { 'type': 'number', 'class': 'sh-input', 'value': '85', 'id': 'temp_critical', 'style': 'width: 100px;' }) + E('input', { 'type': 'number', 'class': 'sh-input', 'value': thresholds.temp_warning || '70', 'id': 'temp_warning', 'style': 'width: 100px;' }), + E('input', { 'type': 'number', 'class': 'sh-input', 'value': thresholds.temp_critical || '85', 'id': 'temp_critical', 'style': 'width: 100px;' }) ]) ]) ]) @@ -86,30 +102,30 @@ return view.extend({ E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '📅'), 'Tâches Planifiées' ]) ]), E('div', { 'class': 'sh-card-body' }, [ - this.renderToggle('📋', 'Rapport Santé Quotidien', 'Tous les jours à 6h00', true, 'sched_health'), - this.renderToggle('💾', 'Sauvegarde Hebdomadaire', 'Dimanche à 3h00, garde 4 versions', true, 'sched_backup'), - this.renderToggle('🧹', 'Nettoyage Logs', 'Supprimer logs > 30 jours', true, 'sched_cleanup') + this.renderToggle('📋', 'Rapport Santé Quotidien', 'Tous les jours à 6h00', schedules.health_report !== false, 'sched_health'), + this.renderToggle('💾', 'Sauvegarde Hebdomadaire', 'Dimanche à 3h00, garde 4 versions', schedules.backup_weekly !== false, 'sched_backup'), + this.renderToggle('🧹', 'Nettoyage Logs', 'Supprimer logs > 30 jours', schedules.log_cleanup !== false, 'sched_cleanup') ]) ]), - + // Upload Configuration E('div', { 'class': 'sh-card' }, [ E('div', { 'class': 'sh-card-header' }, [ E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '☁️'), 'Upload Diagnostics' ]) ]), E('div', { 'class': 'sh-card-body' }, [ - this.renderToggle('☁️', 'Upload automatique', 'Envoyer les diagnostics au support', false, 'cfg_upload'), + this.renderToggle('☁️', 'Upload automatique', 'Envoyer les diagnostics au support', upload.auto_upload === true, 'cfg_upload'), E('div', { 'class': 'sh-form-group', 'style': 'margin-top: 16px;' }, [ E('label', { 'class': 'sh-form-label' }, 'URL d\'upload'), - E('input', { 'type': 'text', 'class': 'sh-input', 'id': 'upload_url', 'placeholder': 'https://support.example.com/upload' }) + E('input', { 'type': 'text', 'class': 'sh-input', 'id': 'upload_url', 'value': upload.url || '', 'placeholder': 'https://support.example.com/upload' }) ]), E('div', { 'class': 'sh-form-group' }, [ E('label', { 'class': 'sh-form-label' }, 'Token d\'authentification'), - E('input', { 'type': 'password', 'class': 'sh-input', 'id': 'upload_token', 'placeholder': '••••••••' }) + E('input', { 'type': 'password', 'class': 'sh-input', 'id': 'upload_token', 'value': upload.token || '', 'placeholder': '••••••••' }) ]) ]) ]), - + // Support Info E('div', { 'class': 'sh-card' }, [ E('div', { 'class': 'sh-card-header' }, [ @@ -118,15 +134,15 @@ return view.extend({ E('div', { 'class': 'sh-card-body' }, [ E('div', { 'class': 'sh-form-group' }, [ E('label', { 'class': 'sh-form-label' }, 'Fournisseur'), - E('input', { 'type': 'text', 'class': 'sh-input', 'id': 'support_provider', 'value': 'CyberMind.fr' }) + E('input', { 'type': 'text', 'class': 'sh-input', 'id': 'support_provider', 'value': support.provider || 'CyberMind.fr' }) ]), E('div', { 'class': 'sh-form-group' }, [ E('label', { 'class': 'sh-form-label' }, 'Email Support'), - E('input', { 'type': 'email', 'class': 'sh-input', 'id': 'support_email', 'value': 'support@cybermind.fr' }) + E('input', { 'type': 'email', 'class': 'sh-input', 'id': 'support_email', 'value': support.email || 'support@cybermind.fr' }) ]), E('div', { 'class': 'sh-form-group' }, [ E('label', { 'class': 'sh-form-label' }, 'URL Documentation'), - E('input', { 'type': 'url', 'class': 'sh-input', 'id': 'support_docs', 'value': 'https://docs.cybermind.fr' }) + E('input', { 'type': 'url', 'class': 'sh-input', 'id': 'support_docs', 'value': support.docs || 'https://docs.cybermind.fr' }) ]) ]) ]), @@ -167,26 +183,47 @@ return view.extend({ E('div', { 'class': 'spinning' }) ]); - uci.load('system-hub').then(function() { - // Save health thresholds - uci.set('system-hub', 'health', 'cpu_warning', document.getElementById('cpu_warning').value); - uci.set('system-hub', 'health', 'cpu_critical', document.getElementById('cpu_critical').value); - uci.set('system-hub', 'health', 'memory_warning', document.getElementById('mem_warning').value); - uci.set('system-hub', 'health', 'memory_critical', document.getElementById('mem_critical').value); - uci.set('system-hub', 'health', 'disk_warning', document.getElementById('disk_warning').value); - uci.set('system-hub', 'health', 'disk_critical', document.getElementById('disk_critical').value); - uci.set('system-hub', 'health', 'temperature_warning', document.getElementById('temp_warning').value); - uci.set('system-hub', 'health', 'temperature_critical', document.getElementById('temp_critical').value); + // Collect all settings from form + var settingsData = { + auto_refresh: document.getElementById('cfg_refresh').classList.contains('active') ? 1 : 0, + health_check: document.getElementById('cfg_health').classList.contains('active') ? 1 : 0, + debug_mode: document.getElementById('cfg_debug').classList.contains('active') ? 1 : 0, + refresh_interval: parseInt(document.getElementById('cfg_interval').value) || 30, + log_retention: parseInt(document.getElementById('cfg_retention').value) || 30, + cpu_warning: parseInt(document.getElementById('cpu_warning').value) || 80, + cpu_critical: parseInt(document.getElementById('cpu_critical').value) || 95, + mem_warning: parseInt(document.getElementById('mem_warning').value) || 80, + mem_critical: parseInt(document.getElementById('mem_critical').value) || 95, + disk_warning: parseInt(document.getElementById('disk_warning').value) || 80, + disk_critical: parseInt(document.getElementById('disk_critical').value) || 95, + temp_warning: parseInt(document.getElementById('temp_warning').value) || 70, + temp_critical: parseInt(document.getElementById('temp_critical').value) || 85 + }; - return uci.save(); - }).then(function() { - return uci.apply(); - }).then(function() { + API.saveSettings( + settingsData.auto_refresh, + settingsData.health_check, + settingsData.debug_mode, + settingsData.refresh_interval, + settingsData.log_retention, + settingsData.cpu_warning, + settingsData.cpu_critical, + settingsData.mem_warning, + settingsData.mem_critical, + settingsData.disk_warning, + settingsData.disk_critical, + settingsData.temp_warning, + settingsData.temp_critical + ).then(function(response) { ui.hideModal(); - ui.addNotification(null, E('p', {}, '✅ Paramètres sauvegardés!'), 'success'); + if (response.success) { + ui.addNotification(null, E('p', {}, 'Paramètres sauvegardés avec succès'), 'success'); + } else { + ui.addNotification(null, E('p', {}, 'Erreur: ' + (response.message || 'Unknown error')), 'error'); + } }).catch(function(err) { ui.hideModal(); - ui.addNotification(null, E('p', {}, '❌ Erreur: ' + err.message), 'error'); + ui.addNotification(null, E('p', {}, 'Erreur: ' + (err.message || err)), 'error'); }); }, diff --git a/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub b/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub index b3577b8..a4d7fc1 100755 --- a/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub +++ b/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub @@ -1,6 +1,7 @@ #!/bin/sh # System Hub RPCD Backend # Central system control and monitoring +# Version: 0.1.1 . /lib/functions.sh . /usr/share/libubox/jshn.sh @@ -98,25 +99,29 @@ get_system_info() { get_health() { json_init - # CPU info - local cpu_model=$(awk -F': ' '/model name|Processor/ {print $2; exit}' /proc/cpuinfo 2>/dev/null || echo "Unknown") + # CPU usage calculation local cpu_cores=$(grep -c "^processor" /proc/cpuinfo 2>/dev/null || echo 1) - - json_add_object "cpu" - json_add_string "model" "$cpu_model" - json_add_int "cores" "$cpu_cores" - json_close_object - - # Load average local load=$(cat /proc/loadavg 2>/dev/null || echo "0 0 0") local load1=$(echo $load | awk '{print $1}') local load5=$(echo $load | awk '{print $2}') local load15=$(echo $load | awk '{print $3}') - json_add_object "load" - json_add_string "1min" "$load1" - json_add_string "5min" "$load5" - json_add_string "15min" "$load15" + # Calculate CPU usage percentage (load / cores * 100) + local cpu_usage=$(awk -v load="$load1" -v cores="$cpu_cores" 'BEGIN { printf "%.0f", (load / cores) * 100 }') + [ "$cpu_usage" -gt 100 ] && cpu_usage=100 + + # CPU status + local cpu_status="ok" + [ "$cpu_usage" -ge 80 ] && cpu_status="warning" + [ "$cpu_usage" -ge 95 ] && cpu_status="critical" + + json_add_object "cpu" + json_add_int "usage" "$cpu_usage" + json_add_string "status" "$cpu_status" + json_add_string "load_1m" "$load1" + json_add_string "load_5m" "$load5" + json_add_string "load_15m" "$load15" + json_add_int "cores" "$cpu_cores" json_close_object # Memory @@ -126,12 +131,17 @@ get_health() { local mem_buffers=$(awk '/Buffers/ {print $2}' /proc/meminfo 2>/dev/null || echo 0) local mem_cached=$(awk '/^Cached/ {print $2}' /proc/meminfo 2>/dev/null || echo 0) local mem_used=$((mem_total - mem_available)) - - local mem_percent=0 + + local mem_usage=0 if [ "$mem_total" -gt 0 ]; then - mem_percent=$(( (mem_used * 100) / mem_total )) + mem_usage=$(( (mem_used * 100) / mem_total )) fi + # Memory status + local mem_status="ok" + [ "$mem_usage" -ge 80 ] && mem_status="warning" + [ "$mem_usage" -ge 95 ] && mem_status="critical" + json_add_object "memory" json_add_int "total_kb" "$mem_total" json_add_int "free_kb" "$mem_free" @@ -139,38 +149,136 @@ get_health() { json_add_int "used_kb" "$mem_used" json_add_int "buffers_kb" "$mem_buffers" json_add_int "cached_kb" "$mem_cached" - json_add_int "percent" "$mem_percent" + json_add_int "usage" "$mem_usage" + json_add_string "status" "$mem_status" json_close_object - # Storage - json_add_array "storage" - df -h | tail -n +2 | while read filesystem size used avail percent mountpoint; do - local percent_num=$(echo $percent | tr -d '%') - json_add_object "" - json_add_string "filesystem" "$filesystem" - json_add_string "size" "$size" - json_add_string "used" "$used" - json_add_string "available" "$avail" - json_add_int "percent" "$percent_num" - json_add_string "mountpoint" "$mountpoint" - json_close_object - done - json_close_array + # Disk (root filesystem) + local disk_total=$(df / | awk 'NR==2 {print $2}') + local disk_used=$(df / | awk 'NR==2 {print $3}') + local disk_usage=$(df / | awk 'NR==2 {gsub("%","",$5); print $5}' 2>/dev/null || echo 0) + + # Disk status + local disk_status="ok" + [ "$disk_usage" -ge 80 ] && disk_status="warning" + [ "$disk_usage" -ge 95 ] && disk_status="critical" + + json_add_object "disk" + json_add_int "total_kb" "$disk_total" + json_add_int "used_kb" "$disk_used" + json_add_int "usage" "$disk_usage" + json_add_string "status" "$disk_status" + json_close_object # Temperature - json_add_array "temperatures" + local temp_value=0 + local temp_status="ok" for zone in /sys/class/thermal/thermal_zone*/temp; do if [ -f "$zone" ]; then local temp=$(cat "$zone" 2>/dev/null || echo 0) local temp_c=$((temp / 1000)) - local zone_name=$(basename $(dirname "$zone")) - - json_add_object "" - json_add_string "zone" "$zone_name" - json_add_int "celsius" "$temp_c" - json_close_object + # Use the highest temperature + [ "$temp_c" -gt "$temp_value" ] && temp_value="$temp_c" fi done + + [ "$temp_value" -ge 70 ] && temp_status="warning" + [ "$temp_value" -ge 85 ] && temp_status="critical" + + json_add_object "temperature" + json_add_int "value" "$temp_value" + json_add_string "status" "$temp_status" + json_close_object + + # Network (WAN connectivity) + local wan_up=0 + local wan_status="error" + if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then + wan_up=1 + wan_status="ok" + fi + + json_add_object "network" + json_add_boolean "wan_up" "$wan_up" + json_add_string "status" "$wan_status" + json_close_object + + # Services + local running_count=0 + local failed_count=0 + for service in /etc/init.d/*; do + [ -x "$service" ] || continue + local name=$(basename "$service") + case "$name" in + boot|done|functions|rc.*|sysctl|umount) continue ;; + esac + + if [ -f "/etc/rc.d/S"*"$name" ]; then + if "$service" running >/dev/null 2>&1; then + running_count=$((running_count + 1)) + else + failed_count=$((failed_count + 1)) + fi + fi + done + + json_add_object "services" + json_add_int "running" "$running_count" + json_add_int "failed" "$failed_count" + json_close_object + + # Calculate overall health score + local score=100 + + # CPU impact (max -30) + if [ "$cpu_usage" -ge 95 ]; then + score=$((score - 30)) + elif [ "$cpu_usage" -ge 80 ]; then + score=$((score - 15)) + elif [ "$cpu_usage" -ge 60 ]; then + score=$((score - 5)) + fi + + # Memory impact (max -25) + if [ "$mem_usage" -ge 95 ]; then + score=$((score - 25)) + elif [ "$mem_usage" -ge 80 ]; then + score=$((score - 12)) + elif [ "$mem_usage" -ge 60 ]; then + score=$((score - 5)) + fi + + # Disk impact (max -20) + if [ "$disk_usage" -ge 95 ]; then + score=$((score - 20)) + elif [ "$disk_usage" -ge 80 ]; then + score=$((score - 10)) + fi + + # Temperature impact (max -15) + if [ "$temp_value" -ge 85 ]; then + score=$((score - 15)) + elif [ "$temp_value" -ge 70 ]; then + score=$((score - 7)) + fi + + # Network impact (max -10) + [ "$wan_up" -eq 0 ] && score=$((score - 10)) + + # Services impact (max -10) + [ "$failed_count" -gt 0 ] && score=$((score - 10)) + + json_add_int "score" "$score" + json_add_string "timestamp" "$(date '+%Y-%m-%d %H:%M:%S')" + + # Recommendations + json_add_array "recommendations" + [ "$cpu_usage" -ge 80 ] && json_add_string "" "CPU usage is high ($cpu_usage%). Consider closing unnecessary services." + [ "$mem_usage" -ge 80 ] && json_add_string "" "Memory usage is high ($mem_usage%). Check for memory leaks." + [ "$disk_usage" -ge 80 ] && json_add_string "" "Disk usage is high ($disk_usage%). Clean up old files or logs." + [ "$temp_value" -ge 70 ] && json_add_string "" "Temperature is elevated (${temp_value}°C). Ensure proper ventilation." + [ "$wan_up" -eq 0 ] && json_add_string "" "WAN connection is down. Check network connectivity." + [ "$failed_count" -gt 0 ] && json_add_string "" "$failed_count service(s) enabled but not running. Check service status." json_close_array json_dump @@ -393,7 +501,7 @@ get_storage() { df -h | tail -n +2 | while read filesystem size used avail percent mountpoint; do local percent_num=$(echo $percent | tr -d '%') - + json_add_object "" json_add_string "filesystem" "$filesystem" json_add_string "size" "$size" @@ -408,6 +516,143 @@ get_storage() { json_dump } +# Get settings +get_settings() { + json_init + + # Load UCI config if it exists + local config_loaded=0 + if [ -f "/etc/config/system-hub" ]; then + config_load system-hub + config_loaded=1 + fi + + # General settings + json_add_object "general" + config_get auto_refresh general auto_refresh "1" + config_get health_check general health_check "1" + config_get debug_mode general debug_mode "0" + config_get refresh_interval general refresh_interval "30" + config_get log_retention general log_retention "30" + json_add_boolean "auto_refresh" "${auto_refresh:-1}" + json_add_boolean "health_check" "${health_check:-1}" + json_add_boolean "debug_mode" "${debug_mode:-0}" + json_add_int "refresh_interval" "${refresh_interval:-30}" + json_add_int "log_retention" "${log_retention:-30}" + json_close_object + + # Alert thresholds + json_add_object "thresholds" + config_get cpu_warning thresholds cpu_warning "80" + config_get cpu_critical thresholds cpu_critical "95" + config_get mem_warning thresholds mem_warning "80" + config_get mem_critical thresholds mem_critical "95" + config_get disk_warning thresholds disk_warning "80" + config_get disk_critical thresholds disk_critical "95" + config_get temp_warning thresholds temp_warning "70" + config_get temp_critical thresholds temp_critical "85" + json_add_int "cpu_warning" "${cpu_warning:-80}" + json_add_int "cpu_critical" "${cpu_critical:-95}" + json_add_int "mem_warning" "${mem_warning:-80}" + json_add_int "mem_critical" "${mem_critical:-95}" + json_add_int "disk_warning" "${disk_warning:-80}" + json_add_int "disk_critical" "${disk_critical:-95}" + json_add_int "temp_warning" "${temp_warning:-70}" + json_add_int "temp_critical" "${temp_critical:-85}" + json_close_object + + # Scheduled tasks + json_add_object "schedules" + config_get health_report schedules health_report "1" + config_get backup_weekly schedules backup_weekly "1" + config_get log_cleanup schedules log_cleanup "1" + json_add_boolean "health_report" "${health_report:-1}" + json_add_boolean "backup_weekly" "${backup_weekly:-1}" + json_add_boolean "log_cleanup" "${log_cleanup:-1}" + json_close_object + + # Upload settings + json_add_object "upload" + config_get auto_upload upload auto_upload "0" + config_get upload_url upload url "" + config_get upload_token upload token "" + json_add_boolean "auto_upload" "${auto_upload:-0}" + json_add_string "url" "${upload_url:-}" + json_add_string "token" "${upload_token:-}" + json_close_object + + # Support info + json_add_object "support" + config_get support_provider support provider "CyberMind.fr" + config_get support_email support email "support@cybermind.fr" + config_get support_docs support docs "https://docs.cybermind.fr" + json_add_string "provider" "${support_provider:-CyberMind.fr}" + json_add_string "email" "${support_email:-support@cybermind.fr}" + json_add_string "docs" "${support_docs:-https://docs.cybermind.fr}" + json_close_object + + json_dump +} + +# Save settings +save_settings() { + read -r input + json_load "$input" + + # Parse settings from input + local section key value + + # Create UCI config if it doesn't exist + if [ ! -f "/etc/config/system-hub" ]; then + touch /etc/config/system-hub + uci set system-hub.general=settings + uci set system-hub.thresholds=thresholds + uci set system-hub.schedules=schedules + uci set system-hub.upload=upload + uci set system-hub.support=support + fi + + # This is a simplified version - in production you'd parse the JSON properly + # For now, we'll extract specific values + json_get_var auto_refresh auto_refresh + json_get_var health_check health_check + json_get_var debug_mode debug_mode + json_get_var refresh_interval refresh_interval + json_get_var log_retention log_retention + json_get_var cpu_warning cpu_warning + json_get_var cpu_critical cpu_critical + json_get_var mem_warning mem_warning + json_get_var mem_critical mem_critical + json_get_var disk_warning disk_warning + json_get_var disk_critical disk_critical + json_get_var temp_warning temp_warning + json_get_var temp_critical temp_critical + + json_cleanup + + # Save to UCI + [ -n "$auto_refresh" ] && uci set system-hub.general.auto_refresh="$auto_refresh" + [ -n "$health_check" ] && uci set system-hub.general.health_check="$health_check" + [ -n "$debug_mode" ] && uci set system-hub.general.debug_mode="$debug_mode" + [ -n "$refresh_interval" ] && uci set system-hub.general.refresh_interval="$refresh_interval" + [ -n "$log_retention" ] && uci set system-hub.general.log_retention="$log_retention" + [ -n "$cpu_warning" ] && uci set system-hub.thresholds.cpu_warning="$cpu_warning" + [ -n "$cpu_critical" ] && uci set system-hub.thresholds.cpu_critical="$cpu_critical" + [ -n "$mem_warning" ] && uci set system-hub.thresholds.mem_warning="$mem_warning" + [ -n "$mem_critical" ] && uci set system-hub.thresholds.mem_critical="$mem_critical" + [ -n "$disk_warning" ] && uci set system-hub.thresholds.disk_warning="$disk_warning" + [ -n "$disk_critical" ] && uci set system-hub.thresholds.disk_critical="$disk_critical" + [ -n "$temp_warning" ] && uci set system-hub.thresholds.temp_warning="$temp_warning" + [ -n "$temp_critical" ] && uci set system-hub.thresholds.temp_critical="$temp_critical" + + uci commit system-hub + + json_init + json_add_boolean "success" 1 + json_add_string "message" "Settings saved successfully" + json_dump +} + # Main dispatcher case "$1" in list) @@ -422,7 +667,23 @@ case "$1" in "backup_config": {}, "restore_config": { "data": "string" }, "reboot": {}, - "get_storage": {} + "get_storage": {}, + "get_settings": {}, + "save_settings": { + "auto_refresh": 1, + "health_check": 1, + "debug_mode": 0, + "refresh_interval": 30, + "log_retention": 30, + "cpu_warning": 80, + "cpu_critical": 95, + "mem_warning": 80, + "mem_critical": 95, + "disk_warning": 80, + "disk_critical": 95, + "temp_warning": 70, + "temp_critical": 85 + } } EOF ;; @@ -438,6 +699,8 @@ EOF restore_config) restore_config ;; reboot) reboot_system ;; get_storage) get_storage ;; + get_settings) get_settings ;; + save_settings) save_settings ;; *) json_init json_add_boolean "success" 0 diff --git a/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json b/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json index 735daac..7fc7e77 100644 --- a/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json +++ b/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json @@ -9,7 +9,8 @@ "get_health", "list_services", "get_logs", - "get_storage" + "get_storage", + "get_settings" ] } }, @@ -19,7 +20,8 @@ "service_action", "backup_config", "restore_config", - "reboot" + "reboot", + "save_settings" ] } }