secubox-openwrt/docs/code-templates.md
CyberMind-FR ef936f1295 docs: Add GitHub Pages documentation site structure
Created comprehensive documentation site using MkDocs Material theme
for GitHub Pages deployment. Moved version sync scripts to secubox-tools.

## Documentation Site (18 new files)

Created docs/ directory with complete documentation:

**Main Pages:**
- index.md - Home page with navigation cards and module overview
- quick-start.md - Quick start guide
- documentation-index.md - Documentation index

**Development Guides:**
- development-guidelines.md - Complete development reference (1857 lines)
- code-templates.md - Working examples and patterns (1405 lines)
- module-implementation-guide.md - Step-by-step workflow (901 lines)

**Reference Documentation:**
- claude.md - Build system and RPCD architecture (553 lines)
- validation-guide.md - Validation workflows (518 lines)
- permissions-guide.md - Permission guidelines (248 lines)
- luci-development-reference.md - LuCI development (1196 lines)

**Module Information:**
- module-status.md - 15 module status (896 lines)
- feature-regeneration-prompts.md - AI prompts (2084 lines)
- todo-analyse.md - Roadmap and tasks (1080 lines)

**Archive (4 files):**
- archive/index.md - Archive index
- archive/build-issues.md - Build troubleshooting
- archive/completion-report.md - Project milestones
- archive/module-enable-disable-design.md - Feature design

**Styling:**
- stylesheets/extra.css - SecuBox custom CSS

## Scripts Reorganization (2 files moved)

Moved version sync utilities to secubox-tools:
- scripts/sync_module_versions.py → secubox-tools/sync_module_versions.py
- scripts/sync_module_versions.sh → secubox-tools/sync_module_versions.sh

## Site Features

- Material theme with dark/light mode
- Responsive design with navigation tabs
- Live search and syntax highlighting
- Custom SecuBox branding (indigo/violet gradients)
- 12,780+ lines of comprehensive documentation

Summary:
- 21 files changed (+12,780 lines)
- 18 new documentation pages
- 2 scripts relocated
- Ready for GitHub Pages deployment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 21:57:29 +01:00

34 KiB

SecuBox Module Code Templates

Version: 1.0.0
Last Updated: 2025-12-28
Status: Active
Purpose: Ready-to-use code templates extracted from working SecuBox modules


See Also


Table of Contents

  1. File Structure Template
  2. API Module Template
  3. JavaScript View Template
  4. RPCD Backend Template
  5. Menu JSON Template
  6. ACL JSON Template
  7. CSS Styling Template
  8. Complete Implementation Example

File Structure Template

Every SecuBox module follows this exact structure:

luci-app-<module-name>/
├── Makefile                                      # OpenWrt package definition
├── README.md                                     # Module documentation
├── htdocs/luci-static/resources/
│   ├── <module-name>/
│   │   ├── api.js                                # RPC API client (REQUIRED)
│   │   ├── theme.js                              # Theme helper (optional)
│   │   └── dashboard.css                         # Module-specific styles
│   └── view/<module-name>/
│       ├── overview.js                           # Main dashboard view
│       ├── settings.js                           # Settings view (if needed)
│       └── *.js                                  # Additional views
└── root/
    ├── etc/config/<module-name>                  # UCI config (optional)
    └── usr/
        ├── libexec/rpcd/
        │   └── luci.<module-name>                # RPCD backend (REQUIRED, must be executable)
        └── share/
            ├── luci/menu.d/
            │   └── luci-app-<module-name>.json   # Menu definition
            └── rpcd/acl.d/
                └── luci-app-<module-name>.json   # ACL permissions

Critical Rules:

  1. RPCD script MUST be named luci.<module-name> (with luci. prefix)
  2. RPCD script MUST be executable (chmod +x)
  3. Menu paths MUST match view file locations
  4. CSS/JS files should be 644 permissions
  5. All ubus objects MUST use luci. prefix

API Module Template

File: htdocs/luci-static/resources/<module-name>/api.js

'use strict';
'require baseclass';
'require rpc';

/**
 * [Module Name] API
 * Package: luci-app-<module-name>
 * RPCD object: luci.<module-name>
 * Version: 1.0.0
 */

// Debug log to verify correct version is loaded
console.log('🔧 [Module Name] API v1.0.0 loaded at', new Date().toISOString());

// ============================================================================
// RPC Method Declarations
// ============================================================================

// Simple method (no parameters)
var callStatus = rpc.declare({
	object: 'luci.<module-name>',  // MUST match RPCD script name
	method: 'status',
	expect: {}
});

// Method with return structure
var callGetData = rpc.declare({
	object: 'luci.<module-name>',
	method: 'get_data',
	expect: { data: [] }  // Expected return structure
});

// Method with parameters
var callPerformAction = rpc.declare({
	object: 'luci.<module-name>',
	method: 'perform_action',
	params: ['action_type', 'target'],  // Parameter names
	expect: { success: false }
});

// Method with multiple parameters
var callUpdateConfig = rpc.declare({
	object: 'luci.<module-name>',
	method: 'update_config',
	params: ['key', 'value', 'persist'],
	expect: {}
});

// ============================================================================
// Helper Functions (Optional)
// ============================================================================

/**
 * Format bytes to human-readable string
 */
function formatBytes(bytes) {
	if (bytes === 0) return '0 B';
	var k = 1024;
	var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
	var i = Math.floor(Math.log(bytes) / Math.log(k));
	return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

/**
 * Format timestamp to "X ago" string
 */
function formatTimeAgo(timestamp) {
	if (!timestamp) return 'Never';
	var now = Math.floor(Date.now() / 1000);
	var diff = now - timestamp;
	if (diff < 60) return diff + 's ago';
	if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
	if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
	return Math.floor(diff / 86400) + 'd ago';
}

/**
 * Format uptime seconds to "Xd Xh Xm" string
 */
function formatUptime(seconds) {
	var days = Math.floor(seconds / 86400);
	var hours = Math.floor((seconds % 86400) / 3600);
	var mins = Math.floor((seconds % 3600) / 60);
	return days + 'd ' + hours + 'h ' + mins + 'm';
}

// ============================================================================
// API Export
// ============================================================================

return baseclass.extend({
	// RPC methods - exposed via ubus
	getStatus: callStatus,
	getData: callGetData,
	performAction: callPerformAction,
	updateConfig: callUpdateConfig,

	// Helper functions
	formatBytes: formatBytes,
	formatTimeAgo: formatTimeAgo,
	formatUptime: formatUptime,

	// Aggregate function for overview page (combines multiple calls)
	getAllData: function() {
		return Promise.all([
			callStatus(),
			callGetData()
		]).then(function(results) {
			return {
				status: results[0] || {},
				data: results[1] || { data: [] }
			};
		});
	}
});

Key Points:

  • Always use 'use strict';
  • Require baseclass and rpc
  • Use rpc.declare() for each RPCD method
  • Export from baseclass.extend()
  • Helper functions can be included in the API module
  • Aggregate functions are useful for views that need multiple data sources

JavaScript View Template

File: htdocs/luci-static/resources/view/<module-name>/overview.js

'use strict';
'require view';
'require ui';
'require dom';
'require poll';
'require <module-name>/api as API';

/**
 * [Module Name] - Overview Dashboard
 * Main view for luci-app-<module-name>
 */

return view.extend({
	// ========================================================================
	// Data Properties
	// ========================================================================

	statusData: null,
	componentData: null,

	// ========================================================================
	// Load Data
	// ========================================================================

	/**
	 * Called when view is loaded
	 * Return a Promise (can use Promise.all for parallel loading)
	 */
	load: function() {
		return Promise.all([
			API.getStatus(),
			API.getData()
		]);
	},

	// ========================================================================
	// Render View
	// ========================================================================

	/**
	 * Called after load() completes
	 * @param {Array} data - Results from load() Promise.all
	 */
	render: function(data) {
		var self = this;
		this.statusData = data[0] || {};
		this.componentData = data[1] || { data: [] };

		// Main container
		var container = E('div', { 'class': 'module-dashboard' }, [
			// Load CSS
			E('link', { 'rel': 'stylesheet', 'href': L.resource('<module-name>/dashboard.css') }),

			// Page Header
			this.renderHeader(),

			// Stats Overview Grid
			this.renderStatsOverview(),

			// Content Cards
			this.renderContent()
		]);

		// Setup auto-refresh polling (30 seconds)
		poll.add(L.bind(function() {
			return Promise.all([
				API.getStatus(),
				API.getData()
			]).then(L.bind(function(refreshData) {
				this.statusData = refreshData[0] || {};
				this.componentData = refreshData[1] || { data: [] };
				this.updateDashboard();
			}, this));
		}, this), 30);

		return container;
	},

	// ========================================================================
	// Render Components
	// ========================================================================

	/**
	 * Render page header with title and stats badges
	 */
	renderHeader: function() {
		return E('div', { 'class': 'sh-page-header' }, [
			E('div', {}, [
				E('h2', { 'class': 'sh-page-title' }, [
					E('span', { 'class': 'sh-page-title-icon' }, '🚀'),
					'Module Title'
				]),
				E('p', { 'class': 'sh-page-subtitle' }, 'Module description and purpose')
			]),
			E('div', { 'class': 'sh-stats-grid' }, [
				this.renderStatBadge('Active Items', this.statusData.active || 0),
				this.renderStatBadge('Total Items', this.statusData.total || 0),
				this.renderStatBadge('Status', this.statusData.status || 'Unknown'),
				this.renderStatBadge('Version', this.statusData.version || '1.0.0')
			])
		]);
	},

	/**
	 * Render a single stat badge
	 */
	renderStatBadge: function(label, value) {
		return E('div', { 'class': 'sh-stat-badge' }, [
			E('div', { 'class': 'sh-stat-value' }, String(value)),
			E('div', { 'class': 'sh-stat-label' }, label)
		]);
	},

	/**
	 * Render stats overview grid
	 */
	renderStatsOverview: function() {
		return E('div', { 'class': 'stats-grid' }, [
			this.renderMetricCard('CPU', this.statusData.cpu),
			this.renderMetricCard('Memory', this.statusData.memory),
			this.renderMetricCard('Disk', this.statusData.disk)
		]);
	},

	/**
	 * Render a metric card with progress bar
	 */
	renderMetricCard: function(title, data) {
		if (!data) return E('div');

		var usage = data.usage || 0;
		var status = usage >= 90 ? 'critical' : (usage >= 75 ? 'warning' : 'ok');
		var color = usage >= 90 ? '#ef4444' : (usage >= 75 ? '#f59e0b' : '#22c55e');

		return E('div', { 'class': 'sh-metric-card sh-metric-' + status }, [
			E('div', { 'class': 'sh-metric-header' }, [
				E('span', { 'class': 'sh-metric-icon' }, this.getMetricIcon(title)),
				E('span', { 'class': 'sh-metric-title' }, title)
			]),
			E('div', { 'class': 'sh-metric-value' }, usage + '%'),
			E('div', { 'class': 'sh-metric-progress' }, [
				E('div', {
					'class': 'sh-metric-progress-bar',
					'style': 'width: ' + usage + '%; background: ' + color
				})
			]),
			E('div', { 'class': 'sh-metric-details' }, data.details || 'N/A')
		]);
	},

	/**
	 * Get icon for metric type
	 */
	getMetricIcon: function(type) {
		switch(type) {
			case 'CPU': return '🔥';
			case 'Memory': return '💾';
			case 'Disk': return '💿';
			default: return '📊';
		}
	},

	/**
	 * Render main content
	 */
	renderContent: function() {
		return E('div', { 'class': 'content-grid' }, [
			this.renderCard('Active Components', this.renderComponentsList()),
			this.renderCard('Quick Actions', this.renderQuickActions()),
			this.renderCard('Recent Activity', this.renderActivityLog())
		]);
	},

	/**
	 * Render a card container
	 */
	renderCard: function(title, content) {
		return E('div', { 'class': 'sh-card' }, [
			E('div', { 'class': 'sh-card-header' }, [
				E('h3', { 'class': 'sh-card-title' }, title)
			]),
			E('div', { 'class': 'sh-card-body' }, content)
		]);
	},

	/**
	 * Render components list
	 */
	renderComponentsList: function() {
		var items = this.componentData.data || [];

		if (items.length === 0) {
			return E('div', { 'class': 'sh-empty-state' }, [
				E('div', { 'class': 'sh-empty-icon' }, '📭'),
				E('div', { 'class': 'sh-empty-text' }, 'No components found')
			]);
		}

		return E('div', { 'class': 'component-list' },
			items.map(L.bind(function(item) {
				return this.renderComponentItem(item);
			}, this))
		);
	},

	/**
	 * Render a single component item
	 */
	renderComponentItem: function(item) {
		var statusClass = item.status === 'active' ? 'sh-card-success' : 'sh-card-warning';

		return E('div', { 'class': 'component-item ' + statusClass }, [
			E('div', { 'class': 'component-name' }, item.name || 'Unknown'),
			E('div', { 'class': 'component-status' }, item.status || 'unknown'),
			E('div', { 'class': 'component-actions' }, [
				E('button', {
					'class': 'sh-btn sh-btn-primary sh-btn-sm',
					'click': L.bind(this.handleAction, this, item.id, 'view')
				}, 'View'),
				E('button', {
					'class': 'sh-btn sh-btn-secondary sh-btn-sm',
					'click': L.bind(this.handleAction, this, item.id, 'configure')
				}, 'Configure')
			])
		]);
	},

	/**
	 * Render quick actions
	 */
	renderQuickActions: function() {
		return E('div', { 'class': 'quick-actions' }, [
			E('button', {
				'class': 'sh-btn sh-btn-primary',
				'click': L.bind(this.handleRefresh, this)
			}, '🔄 Refresh'),
			E('button', {
				'class': 'sh-btn sh-btn-success',
				'click': L.bind(this.handleAction, this, null, 'start_all')
			}, '▶️ Start All'),
			E('button', {
				'class': 'sh-btn sh-btn-danger',
				'click': L.bind(this.handleAction, this, null, 'stop_all')
			}, '⏹️ Stop All')
		]);
	},

	/**
	 * Render activity log
	 */
	renderActivityLog: function() {
		var activities = this.statusData.recent_activities || [];

		if (activities.length === 0) {
			return E('div', { 'class': 'sh-empty-text' }, 'No recent activity');
		}

		return E('div', { 'class': 'activity-log' },
			activities.map(function(activity) {
				return E('div', { 'class': 'activity-item' }, [
					E('span', { 'class': 'activity-time' }, activity.time || ''),
					E('span', { 'class': 'activity-text' }, activity.message || '')
				]);
			})
		);
	},

	// ========================================================================
	// Event Handlers
	// ========================================================================

	/**
	 * Handle generic action
	 */
	handleAction: function(id, action, ev) {
		var self = this;

		ui.showModal(_('Performing Action'), [
			E('p', {}, _('Please wait...'))
		]);

		API.performAction(action, id || '').then(function(result) {
			ui.hideModal();

			if (result.success) {
				ui.addNotification(null, E('p', _('Action completed successfully')), 'success');
				self.handleRefresh();
			} else {
				ui.addNotification(null, E('p', _('Action failed: %s').format(result.message || 'Unknown error')), 'error');
			}
		}).catch(function(error) {
			ui.hideModal();
			ui.addNotification(null, E('p', _('Error: %s').format(error.message || 'Unknown error')), 'error');
		});
	},

	/**
	 * Handle refresh
	 */
	handleRefresh: function() {
		var self = this;

		return Promise.all([
			API.getStatus(),
			API.getData()
		]).then(function(data) {
			self.statusData = data[0] || {};
			self.componentData = data[1] || { data: [] };
			self.updateDashboard();
			ui.addNotification(null, E('p', _('Dashboard refreshed')), 'info');
		});
	},

	/**
	 * Update dashboard without full re-render
	 */
	updateDashboard: function() {
		// Update specific DOM elements instead of full re-render
		var statsGrid = document.querySelector('.sh-stats-grid');
		if (statsGrid) {
			dom.content(statsGrid, [
				this.renderStatBadge('Active Items', this.statusData.active || 0),
				this.renderStatBadge('Total Items', this.statusData.total || 0),
				this.renderStatBadge('Status', this.statusData.status || 'Unknown'),
				this.renderStatBadge('Version', this.statusData.version || '1.0.0')
			]);
		}

		// Update components list
		var componentsList = document.querySelector('.component-list');
		if (componentsList) {
			var items = this.componentData.data || [];
			dom.content(componentsList,
				items.map(L.bind(function(item) {
					return this.renderComponentItem(item);
				}, this))
			);
		}
	},

	// ========================================================================
	// Required LuCI Methods (can be null if not used)
	// ========================================================================

	handleSaveApply: null,
	handleSave: null,
	handleReset: null
});

Key Points:

  • Extend view with load() and render() methods
  • Use E() helper to build DOM elements
  • Use L.bind() for event handlers to preserve this context
  • Use poll.add() for auto-refresh functionality
  • Use dom.content() for efficient partial updates
  • Use ui.showModal(), ui.hideModal(), ui.addNotification() for user feedback
  • Always handle errors in Promise chains

RPCD Backend Template

File: root/usr/libexec/rpcd/luci.<module-name>

#!/bin/sh
# [Module Name] RPCD Backend
# Package: luci-app-<module-name>
# Version: 1.0.0

# Source required libraries
. /lib/functions.sh
. /usr/share/libubox/jshn.sh

# ============================================================================
# RPC Methods
# ============================================================================

# Get status information
status() {
	json_init

	# Example: Get system info
	local hostname=$(cat /proc/sys/kernel/hostname 2>/dev/null || echo "unknown")
	local uptime=$(awk '{print int($1)}' /proc/uptime 2>/dev/null || echo 0)

	json_add_string "hostname" "$hostname"
	json_add_int "uptime" "$uptime"
	json_add_string "version" "1.0.0"
	json_add_string "status" "running"

	# Add nested object
	json_add_object "cpu"
	local cpu_load=$(awk '{print $1}' /proc/loadavg 2>/dev/null || echo "0")
	json_add_string "load" "$cpu_load"
	json_add_int "usage" "45"
	json_close_object

	# Add timestamp
	json_add_string "timestamp" "$(date '+%Y-%m-%d %H:%M:%S')"

	json_dump
}

# Get data with array
get_data() {
	json_init
	json_add_array "data"

	# Example: List files
	for file in /etc/config/*; do
		[ -f "$file" ] || continue
		local name=$(basename "$file")

		json_add_object ""
		json_add_string "name" "$name"
		json_add_string "path" "$file"
		json_add_int "size" "$(stat -c%s "$file" 2>/dev/null || echo 0)"
		json_close_object
	done

	json_close_array
	json_dump
}

# Perform action with parameters
perform_action() {
	# Read JSON input from stdin
	read -r input
	json_load "$input"

	# Extract parameters
	local action_type target
	json_get_var action_type action_type
	json_get_var target target
	json_cleanup

	# Validate parameters
	if [ -z "$action_type" ]; then
		json_init
		json_add_boolean "success" 0
		json_add_string "message" "Action type is required"
		json_dump
		return 1
	fi

	# Perform action based on type
	local result=0
	case "$action_type" in
		start)
			# Example: Start a service
			/etc/init.d/"$target" start >/dev/null 2>&1
			result=$?
			;;
		stop)
			# Example: Stop a service
			/etc/init.d/"$target" stop >/dev/null 2>&1
			result=$?
			;;
		restart)
			# Example: Restart a service
			/etc/init.d/"$target" restart >/dev/null 2>&1
			result=$?
			;;
		*)
			json_init
			json_add_boolean "success" 0
			json_add_string "message" "Invalid action: $action_type"
			json_dump
			return 1
			;;
	esac

	# Return result
	json_init
	if [ "$result" -eq 0 ]; then
		json_add_boolean "success" 1
		json_add_string "message" "Action '$action_type' completed successfully"
	else
		json_add_boolean "success" 0
		json_add_string "message" "Action '$action_type' failed"
	fi
	json_dump
}

# Update configuration
update_config() {
	read -r input
	json_load "$input"

	local key value persist
	json_get_var key key
	json_get_var value value
	json_get_var persist persist
	json_cleanup

	# Validate
	if [ -z "$key" ] || [ -z "$value" ]; then
		json_init
		json_add_boolean "success" 0
		json_add_string "message" "Key and value are required"
		json_dump
		return 1
	fi

	# Update UCI config
	uci set <module-name>.general."$key"="$value"

	if [ "$persist" = "1" ]; then
		uci commit <module-name>
	fi

	json_init
	json_add_boolean "success" 1
	json_add_string "message" "Configuration updated"
	json_dump
}

# ============================================================================
# Main Dispatcher
# ============================================================================

case "$1" in
	list)
		# List all available methods with their parameters
		cat << 'EOF'
{
	"status": {},
	"get_data": {},
	"perform_action": {
		"action_type": "string",
		"target": "string"
	},
	"update_config": {
		"key": "string",
		"value": "string",
		"persist": 1
	}
}
EOF
		;;
	call)
		# Route to the appropriate method
		case "$2" in
			status) status ;;
			get_data) get_data ;;
			perform_action) perform_action ;;
			update_config) update_config ;;
			*)
				# Unknown method
				json_init
				json_add_boolean "success" 0
				json_add_string "error" "Unknown method: $2"
				json_dump
				;;
		esac
		;;
esac

Key Points:

  • Always start with #!/bin/sh
  • Source /lib/functions.sh and /usr/share/libubox/jshn.sh
  • Use json_init, json_add_*, json_dump for JSON output
  • Read parameters from stdin using read -r input and json_load
  • Always validate input parameters
  • Return proper success/error status
  • Implement list case to declare all methods and parameters
  • Implement call case to route to method handlers

Menu JSON Template

File: root/usr/share/luci/menu.d/luci-app-<module-name>.json

{
	"admin/secubox/<category>/<module-name>": {
		"title": "Module Title",
		"order": 10,
		"action": {
			"type": "firstchild"
		},
		"depends": {
			"acl": ["luci-app-<module-name>"]
		}
	},
	"admin/secubox/<category>/<module-name>/overview": {
		"title": "Overview",
		"order": 1,
		"action": {
			"type": "view",
			"path": "<module-name>/overview"
		}
	},
	"admin/secubox/<category>/<module-name>/settings": {
		"title": "Settings",
		"order": 2,
		"action": {
			"type": "view",
			"path": "<module-name>/settings"
		}
	}
}

Categories:

  • security - Security & monitoring modules (CrowdSec, Auth Guardian)
  • monitoring - Monitoring modules (Netdata)
  • network - Network modules (Netifyd, Network Modes, WireGuard)
  • system - System modules (System Hub)
  • services - Service modules (CDN Cache, VHost Manager)

Key Points:

  • Menu paths follow admin/secubox/<category>/<module-name>
  • First entry uses "type": "firstchild" to redirect to first child
  • Subsequent entries use "type": "view" with "path" matching view file location
  • Path MUST match: "path": "<module-name>/overview"view/<module-name>/overview.js
  • Order determines menu position (lower numbers first)
  • Depends on ACL entry matching package name

ACL JSON Template

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

{
	"luci-app-<module-name>": {
		"description": "Module Title - Brief Description",
		"read": {
			"ubus": {
				"luci.<module-name>": [
					"status",
					"get_data",
					"get_config",
					"list_items"
				]
			},
			"uci": ["<module-name>"]
		},
		"write": {
			"ubus": {
				"luci.<module-name>": [
					"perform_action",
					"update_config",
					"delete_item",
					"restart_service"
				]
			},
			"uci": ["<module-name>"]
		}
	}
}

Key Points:

  • ACL entry name MUST match package name
  • read section lists methods that can be called without write permissions
  • write section lists methods that modify state
  • ubus object names MUST match RPCD script name (luci.<module-name>)
  • UCI config access can be granted separately
  • All method names MUST exactly match those defined in RPCD script

CSS Styling Template

File: htdocs/luci-static/resources/<module-name>/dashboard.css

/**
 * [Module Name] Dashboard Styles
 * Extends system-hub/common.css design system
 * Version: 1.0.0
 */

/* ============================================================================
   IMPORTANT: Import common.css for design system variables
   ============================================================================ */
@import url('../system-hub/common.css');

/* ============================================================================
   Module-Specific Styles
   ============================================================================ */

/* Container */
.module-dashboard {
	padding: 24px;
	background: var(--sh-bg-primary);
	min-height: 100vh;
}

/* Stats Grid */
.stats-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
	gap: 20px;
	margin: 24px 0;
}

/* Metric Card */
.sh-metric-card {
	background: var(--sh-bg-card);
	border: 1px solid var(--sh-border);
	border-radius: 16px;
	padding: 24px;
	transition: all 0.3s ease;
	position: relative;
	overflow: hidden;
}

.sh-metric-card::before {
	content: '';
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	height: 3px;
	background: linear-gradient(90deg, var(--sh-primary), var(--sh-primary-end));
	opacity: 0;
	transition: opacity 0.3s ease;
}

.sh-metric-card:hover {
	transform: translateY(-3px);
	box-shadow: 0 12px 28px var(--sh-hover-shadow);
}

.sh-metric-card:hover::before {
	opacity: 1;
}

/* Metric status variants */
.sh-metric-ok::before {
	background: var(--sh-success);
	opacity: 1;
}

.sh-metric-warning::before {
	background: var(--sh-warning);
	opacity: 1;
}

.sh-metric-critical::before {
	background: var(--sh-danger);
	opacity: 1;
}

/* Metric Header */
.sh-metric-header {
	display: flex;
	align-items: center;
	gap: 12px;
	margin-bottom: 16px;
}

.sh-metric-icon {
	font-size: 28px;
	line-height: 1;
}

.sh-metric-title {
	font-size: 16px;
	font-weight: 600;
	color: var(--sh-text-secondary);
}

/* Metric Value */
.sh-metric-value {
	font-size: 40px;
	font-weight: 700;
	font-family: 'JetBrains Mono', monospace;
	color: var(--sh-text-primary);
	margin-bottom: 12px;
}

/* Metric Progress Bar */
.sh-metric-progress {
	width: 100%;
	height: 8px;
	background: var(--sh-bg-tertiary);
	border-radius: 4px;
	overflow: hidden;
	margin-bottom: 8px;
}

.sh-metric-progress-bar {
	height: 100%;
	background: var(--sh-primary);
	transition: width 0.5s ease;
	border-radius: 4px;
}

/* Metric Details */
.sh-metric-details {
	font-size: 14px;
	color: var(--sh-text-secondary);
	font-weight: 500;
}

/* Content Grid */
.content-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
	gap: 24px;
	margin-top: 24px;
}

/* Component List */
.component-list {
	display: flex;
	flex-direction: column;
	gap: 12px;
}

.component-item {
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: 16px;
	background: var(--sh-bg-secondary);
	border: 1px solid var(--sh-border);
	border-radius: 12px;
	transition: all 0.2s ease;
	position: relative;
	overflow: hidden;
}

.component-item::before {
	content: '';
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	height: 3px;
	background: var(--sh-primary);
	opacity: 0;
	transition: opacity 0.3s ease;
}

.component-item:hover {
	transform: translateX(4px);
	border-color: var(--sh-primary);
}

.component-item:hover::before {
	opacity: 1;
}

.component-name {
	font-size: 16px;
	font-weight: 600;
	color: var(--sh-text-primary);
	flex: 1;
}

.component-status {
	font-size: 14px;
	color: var(--sh-text-secondary);
	margin: 0 16px;
}

.component-actions {
	display: flex;
	gap: 8px;
}

/* Quick Actions */
.quick-actions {
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
}

/* Activity Log */
.activity-log {
	display: flex;
	flex-direction: column;
	gap: 12px;
}

.activity-item {
	display: flex;
	align-items: center;
	gap: 12px;
	padding: 12px;
	background: var(--sh-bg-secondary);
	border-radius: 8px;
	font-size: 14px;
}

.activity-time {
	font-family: 'JetBrains Mono', monospace;
	color: var(--sh-text-secondary);
	font-size: 12px;
	min-width: 80px;
}

.activity-text {
	color: var(--sh-text-primary);
	flex: 1;
}

/* ============================================================================
   Button Variants (Small Size)
   ============================================================================ */

.sh-btn-sm {
	padding: 6px 12px;
	font-size: 12px;
}

/* ============================================================================
   Responsive Design
   ============================================================================ */

@media (max-width: 768px) {
	.module-dashboard {
		padding: 16px;
	}

	.stats-grid,
	.content-grid {
		grid-template-columns: 1fr;
	}

	.component-item {
		flex-direction: column;
		align-items: flex-start;
		gap: 12px;
	}

	.component-actions {
		width: 100%;
	}

	.sh-metric-value {
		font-size: 32px;
	}
}

/* ============================================================================
   Dark Mode Enhancements
   ============================================================================ */

[data-theme="dark"] .module-dashboard {
	background: var(--sh-bg-primary);
}

[data-theme="dark"] .component-item,
[data-theme="dark"] .activity-item {
	background: var(--sh-bg-secondary);
	border-color: var(--sh-border);
}

[data-theme="dark"] .component-item:hover {
	background: var(--sh-bg-tertiary);
}

Key Points:

  • Always import system-hub/common.css for design system variables
  • Use CSS variables (var(--sh-*)) instead of hardcoded colors
  • Support dark mode with [data-theme="dark"] selectors
  • Use responsive grid layouts (grid-template-columns: repeat(auto-fit, minmax(...)))
  • Add smooth transitions for better UX
  • Use JetBrains Mono for numeric values
  • Follow mobile-first responsive design

Complete Implementation Example

Here's a complete minimal working example for a new module called "Example Dashboard":

Directory Structure

luci-app-example-dashboard/
├── Makefile
├── htdocs/luci-static/resources/
│   ├── example-dashboard/
│   │   ├── api.js
│   │   └── dashboard.css
│   └── view/example-dashboard/
│       └── overview.js
└── root/
    └── usr/
        ├── libexec/rpcd/
        │   └── luci.example-dashboard
        └── share/
            ├── luci/menu.d/
            │   └── luci-app-example-dashboard.json
            └── rpcd/acl.d/
                └── luci-app-example-dashboard.json

api.js

'use strict';
'require baseclass';
'require rpc';

var callStatus = rpc.declare({
	object: 'luci.example-dashboard',
	method: 'status',
	expect: {}
});

return baseclass.extend({
	getStatus: callStatus
});

overview.js

'use strict';
'require view';
'require example-dashboard/api as API';

return view.extend({
	load: function() {
		return API.getStatus();
	},

	render: function(data) {
		return E('div', {}, [
			E('link', { 'rel': 'stylesheet', 'href': L.resource('example-dashboard/dashboard.css') }),
			E('h2', {}, 'Example Dashboard'),
			E('p', {}, 'Status: ' + (data.status || 'Unknown'))
		]);
	},

	handleSaveApply: null,
	handleSave: null,
	handleReset: null
});

luci.example-dashboard (RPCD)

#!/bin/sh
. /usr/share/libubox/jshn.sh

status() {
	json_init
	json_add_string "status" "running"
	json_add_string "version" "1.0.0"
	json_dump
}

case "$1" in
	list)
		echo '{"status":{}}'
		;;
	call)
		case "$2" in
			status) status ;;
		esac
		;;
esac

menu.d/luci-app-example-dashboard.json

{
	"admin/secubox/monitoring/example-dashboard": {
		"title": "Example Dashboard",
		"order": 50,
		"action": {
			"type": "firstchild"
		},
		"depends": {
			"acl": ["luci-app-example-dashboard"]
		}
	},
	"admin/secubox/monitoring/example-dashboard/overview": {
		"title": "Overview",
		"order": 1,
		"action": {
			"type": "view",
			"path": "example-dashboard/overview"
		}
	}
}

acl.d/luci-app-example-dashboard.json

{
	"luci-app-example-dashboard": {
		"description": "Example Dashboard",
		"read": {
			"ubus": {
				"luci.example-dashboard": ["status"]
			}
		}
	}
}

dashboard.css

@import url('../system-hub/common.css');

div {
	padding: 20px;
}

Installation Steps:

  1. Copy files to module directory
  2. Set RPCD permissions: chmod +x root/usr/libexec/rpcd/luci.example-dashboard
  3. Validate: ./secubox-tools/validate-modules.sh
  4. Build: ./secubox-tools/local-build.sh build luci-app-example-dashboard
  5. Deploy: scp build/x86-64/*.ipk root@router:/tmp/
  6. Install: ssh root@router "opkg install /tmp/luci-app-example-dashboard*.ipk && /etc/init.d/rpcd restart"

Common Pitfalls and Solutions

1. RPCD "Object not found" Error

Error: RPC call to luci.example-dashboard/status failed with error -32000: Object not found

Solutions:

  • Check RPCD script name matches ubus object name exactly (including luci. prefix)
  • Verify RPCD script is executable: chmod +x root/usr/libexec/rpcd/luci.example-dashboard
  • Restart RPCD service: /etc/init.d/rpcd restart
  • Check RPCD logs: logread | grep rpcd

2. HTTP 404 View Not Found

Error: HTTP error 404 while loading class file '/luci-static/resources/view/example-dashboard/overview.js'

Solutions:

  • Verify menu path matches view file location exactly
  • Menu: "path": "example-dashboard/overview" → File: view/example-dashboard/overview.js
  • Check file permissions: 644 for CSS/JS files
  • Clear browser cache

3. ACL Permission Denied

Error: Access denied or missing permissions

Solutions:

  • Verify ACL file exists in root/usr/share/rpcd/acl.d/
  • Check ubus object name in ACL matches RPCD script name
  • Restart RPCD: /etc/init.d/rpcd restart
  • Check method is listed in appropriate section (read vs write)

4. CSS Not Loading

Problem: Styles not applied

Solutions:

  • Verify CSS import path: L.resource('example-dashboard/dashboard.css')
  • Check file permissions: 644
  • Import common.css: @import url('../system-hub/common.css');
  • Clear browser cache
  • Check browser console for 404 errors

5. Auto-Refresh Not Working

Problem: Poll not updating dashboard

Solutions:

  • Verify poll.add() is called in render() method
  • Check API calls in poll callback return Promises
  • Ensure updateDashboard() method updates DOM correctly
  • Use browser console to check for JavaScript errors

Validation Checklist

Before deploying, always run these checks:

# 1. Fix permissions
./secubox-tools/fix-permissions.sh --local

# 2. Validate module structure
./secubox-tools/validate-modules.sh

# 3. Validate JSON syntax
find luci-app-example-dashboard -name "*.json" -exec jsonlint {} \;

# 4. Validate shell scripts
shellcheck luci-app-example-dashboard/root/usr/libexec/rpcd/*

# 5. Build locally
./secubox-tools/local-build.sh build luci-app-example-dashboard

Document Version: 1.0.0 Last Updated: 2025-12-27 Maintainer: CyberMind.fr