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
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.
ubus (OpenWrt micro bus architecture) is OpenWrt’s inter-process communication (IPC) system. It enables:
ubus callCRITICAL 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:
RPC call to system-hub/status failed with error -32000: Object not foundThe 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/
Read Operations (GET-like):
status - Get current stateget_* - 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 modificationsACL Mapping:
"read" section in ACL"write" section in ACLRPCD backends are executable shell scripts that:
$1 for the action (list or call)$2 for the method name (if call)#!/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
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 objectjson_add_string "key" "value" - Add stringjson_add_int "key" 123 - Add integerjson_add_boolean "key" 1 - Add boolean (0 or 1)json_add_object "key" - Start nested objectjson_close_object - End nested objectjson_add_array "key" - Start arrayjson_close_array - End arrayjson_dump - Output JSON to stdoutAlways 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
}
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
}
/proc files multiple times# Good
uptime=$(cat /proc/uptime | cut -d' ' -f1)
# Better
read uptime _ < /proc/uptime
uptime=${uptime%.*}
# Slow
count=$(ls /etc/init.d | wc -l)
# Fast
count=0
for file in /etc/init.d/*; do
[ -f "$file" ] && count=$((count + 1))
done
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()?
'require module/api as API' which auto-instantiatesbaseclass.extend() creates a proper class constructorbaseclass.singleton() breaks the instantiation mechanismvar 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
// Method returns single object
expect: {}
// Method returns array at top level
expect: { services: [] }
// Method returns specific structure
expect: {
services: [],
count: 0
}
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;
});
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 classesL.require('module.path') (with dots) returns raw class constructorbaseclass, which needs instantiationas VAR pattern'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
});
// 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 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
{
"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 Operations (no system modification):
status - Get current stateget_* - 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 changesbackup, restore - System backup/restorereboot, shutdown - System controlError: Access denied or RPC error -32002
Cause: Method not listed in ACL, or listed in wrong section (read vs write)
Solution:
/etc/init.d/rpcd restartValidation:
# 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
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:
usage (percentage) and status (ok/warning/critical)used_kb AND usage percentage)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 availableUse ISO 8601 or consistent local format:
timestamp="$(date '+%Y-%m-%d %H:%M:%S')" # 2025-12-26 10:30:00
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
}
Use arrays for:
Use single values for:
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"
}
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:
grep -r "object:" luci-app-system-hub/htdocs --include="*.js"
Output: object: 'luci.system-hub'
mv root/usr/libexec/rpcd/system-hub root/usr/libexec/rpcd/luci.system-hub
chmod +x root/usr/libexec/rpcd/luci.system-hub
/etc/init.d/rpcd restart
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.
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:
Open ACL file: root/usr/share/rpcd/acl.d/luci-app-system-hub.json
"read": {
"ubus": {
"luci.system-hub": [
"get_settings"
]
}
}
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"
Error: Dashboard shows “NaN%”, “undefined”, or empty values
Cause: Frontend using wrong data structure keys (outdated after backend changes)
Solution:
ubus call luci.system-hub get_health
// ❌ 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;
var temp = health.temperature?.value || 0;
var loadAvg = health.cpu?.load_1m || '0.00';
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:
cat root/usr/share/luci/menu.d/luci-app-netifyd-dashboard.json
Look for: "path": "netifyd/overview"
ls htdocs/luci-static/resources/view/
File is at: view/netifyd-dashboard/overview.js
// 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/
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({...})return {...}baseclass.prototypeSymptom: Changes to RPCD script don’t take effect
Solution:
ssh router "ls -la /usr/libexec/rpcd/"
ssh router "chmod +x /usr/libexec/rpcd/luci.system-hub"
ssh router "/etc/init.d/rpcd restart"
Clear browser cache (Ctrl+Shift+R)
ssh router "logread | grep rpcd"
Use this checklist before deployment:
/usr/libexec/rpcd/luci.<module-name>chmod +x/usr/share/luci/menu.d/luci-app-<module>.json/usr/share/rpcd/acl.d/luci-app-<module>.jsonhtdocs/luci-static/resources/<module>/api.jshtdocs/luci-static/resources/view/<module>/*.jsluci. prefix)luci."luci-app-<module>"baseclass.extend() pattern'require <module>/api as API' patternobject, method, params, expectubus call)jsonlint)jsonlint)"read" section"write" section./secubox-tools/validate-modules.shubus call luci.<module> <method>/etc/init.d/rpcd restart# 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/*
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
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)"
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"
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
✅ 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
❌ 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)
v1.0 (2025-12-26)
/usr/share/libubox/jshn.sh on OpenWrtFor questions or contributions to this reference guide:
END OF REFERENCE GUIDE