Skip to content

# LuCI Development Reference Guide

Version: 1.0.0
Last Updated: 2025-12-28
Status: Active

Version: 1.0.0
Last Updated: 2025-12-28
Status: Active
Based on: luci-app-secubox and luci-app-system-hub implementations
Target Audience: Claude.ai and developers working on OpenWrt LuCI applications

## See Also

- Design & Standards: DEVELOPMENT-GUIDELINES.md
- Quick Commands: QUICK-START.md
- Automation Briefing: CODEX.md
- Code Templates: CODE-TEMPLATES.md

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
2. RPCD Backend Patterns
3. LuCI API Module Patterns
4. LuCI View Import Patterns
5. ACL Permission Structure
6. Data Structure Conventions
7. Common Errors and Solutions
8. Validation Checklist
9. 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.

    // ✅ 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:

    # 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:

    # 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

    #!/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:

    # 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:

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

    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:

       # Good
       uptime=$(cat /proc/uptime | cut -d' ' -f1)

       # Better
       read uptime _ < /proc/uptime
       uptime=${uptime%.*}

3. Avoid external commands when possible:

       # 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.

    '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

    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:

    // 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

    // 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:

    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.

    // ✅ 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

    '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

    // 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-<module-name>.json

In source tree:

    luci-app-<module-name>/root/usr/share/rpcd/acl.d/luci-app-<module-name>.json

### Standard ACL Template

    {
        "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:

    # 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:

    {
        "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:

    timestamp="$(date '+%Y-%m-%d %H:%M:%S')"  # 2025-12-26 10:30:00

### Boolean Values in JSON

In shell scripts using jshn.sh:

    json_add_boolean "wan_up" 1  # true
    json_add_boolean "wan_up" 0  # false

In 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:

    // 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:

    grep -r "object:" luci-app-system-hub/htdocs --include="*.js"

Output: `object: 'luci.system-hub'`

1. Rename RPCD script to match exactly:

       mv root/usr/libexec/rpcd/system-hub root/usr/libexec/rpcd/luci.system-hub

2. Ensure script is executable:

       chmod +x root/usr/libexec/rpcd/luci.system-hub

3. Restart RPCD:

       /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:

    var api = L.require('system-hub.api');  // ❌ Wrong

To:

    '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`

1. Add method to appropriate section:

       "read": {
           "ubus": {
               "luci.system-hub": [
                   "get_settings"
               ]
           }
       }

2. Deploy and restart RPCD:

       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:

    ubus call luci.system-hub get_health

1. Update frontend to match structure:

       // ❌ 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;

2. Add null/undefined checks:

       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:

    cat root/usr/share/luci/menu.d/luci-app-netifyd-dashboard.json

Look for: `"path": "netifyd/overview"`

1. Check actual file location:

       ls htdocs/luci-static/resources/view/

   File is at: `view/netifyd-dashboard/overview.js`

2. Fix either menu path OR file location:

       // 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()`:

    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:

    ssh router "ls -la /usr/libexec/rpcd/"

1. Check script is executable:

       ssh router "chmod +x /usr/libexec/rpcd/luci.system-hub"

2. Restart RPCD:

       ssh router "/etc/init.d/rpcd restart"

3. Clear browser cache (Ctrl+Shift+R)

4. Check RPCD logs:

       ssh router "logread | grep rpcd"

## Validation Checklist

Use this checklist before deployment:

### File Structure

- RPCD script exists: `/usr/libexec/rpcd/luci.<module-name>`
- RPCD script is executable: `chmod +x`
- Menu JSON exists: `/usr/share/luci/menu.d/luci-app-<module>.json`
- ACL JSON exists: `/usr/share/rpcd/acl.d/luci-app-<module>.json`
- API module exists: `htdocs/luci-static/resources/<module>/api.js`
- Views exist: `htdocs/luci-static/resources/view/<module>/*.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-<module>"`

### Code Validation

- API module uses `baseclass.extend()` pattern
- Views import API with `'require <module>/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.<module> <method>`
- Test frontend in browser (check console for errors)
- Clear browser cache after deployment
- Verify RPCD restart: `/etc/init.d/rpcd restart`

### Automated Validation Command

    # 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:

    # 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:

    #!/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:
4. RPC errors (object not found, method not found, access denied)
5. JavaScript errors (api.method is not a function)
6. 404 errors (view files not found)
7. Test functionality:
8. Load data displays correctly
9. Actions work (start/stop services, save settings)
10. No "NaN", "undefined", or empty values

### Remote ubus Testing

Test RPCD methods on router:

    # 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:

    # 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:

    // 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:

    # 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 contact@cybermind.fr - Project: SecuBox OpenWrt - Repository: https://github.com/cybermind-fr/secubox-openwrt

END OF REFERENCE GUIDE