secubox-openwrt/docs-fr/code-templates.md
CyberMind-FR ccfb58124c docs: Add trilingual documentation (French and Chinese translations)
Add complete French (fr) and Chinese (zh) translations for all documentation:

- Root files: README, CHANGELOG, SECURITY, BETA-RELEASE
- docs/: All 16 core documentation files
- DOCS/: All 19 deep-dive documents including embedded/ and archive/
- package/secubox/: All 123+ package READMEs
- Misc: secubox-tools/, scripts/, EXAMPLES/, config-backups/, streamlit-apps/

Total: 346 translation files created

Each file includes language switcher links for easy navigation between
English, French, and Chinese versions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-20 10:00:18 +01:00

35 KiB

Modeles de Code pour Modules SecuBox

Langues: English | Francais | 中文

Version: 1.0.0 Derniere mise a jour: 2025-12-28 Statut: Actif Objectif: Modeles de code prets a l'emploi extraits de modules SecuBox fonctionnels


Voir aussi


Table des matieres

  1. Modele de structure de fichiers
  2. Modele de module API
  3. Modele de vue JavaScript
  4. Modele de backend RPCD
  5. Modele JSON de menu
  6. Modele JSON ACL
  7. Modele de style CSS
  8. Exemple d'implementation complete

Modele de structure de fichiers

Chaque module SecuBox suit cette structure exacte:

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

Regles critiques:

  1. Le script RPCD DOIT etre nomme luci.<module-name> (avec le prefixe luci.)
  2. Le script RPCD DOIT etre executable (chmod +x)
  3. Les chemins de menu DOIVENT correspondre aux emplacements des fichiers de vue
  4. Les fichiers CSS/JS doivent avoir les permissions 644
  5. Tous les objets ubus DOIVENT utiliser le prefixe luci.

Modele de module API

Fichier: 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: [] }
			};
		});
	}
});

Points cles:

  • Toujours utiliser 'use strict';
  • Requiert baseclass et rpc
  • Utiliser rpc.declare() pour chaque methode RPCD
  • Exporter depuis baseclass.extend()
  • Les fonctions utilitaires peuvent etre incluses dans le module API
  • Les fonctions d'agregation sont utiles pour les vues necessitant plusieurs sources de donnees

Modele de vue JavaScript

Fichier: 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
});

Points cles:

  • Etendre view avec les methodes load() et render()
  • Utiliser l'utilitaire E() pour construire les elements DOM
  • Utiliser L.bind() pour les gestionnaires d'evenements afin de preserver le contexte this
  • Utiliser poll.add() pour la fonctionnalite de rafraichissement automatique
  • Utiliser dom.content() pour des mises a jour partielles efficaces
  • Utiliser ui.showModal(), ui.hideModal(), ui.addNotification() pour le retour utilisateur
  • Toujours gerer les erreurs dans les chaines de Promise

Modele de backend RPCD

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

Points cles:

  • Toujours commencer par #!/bin/sh
  • Sourcer /lib/functions.sh et /usr/share/libubox/jshn.sh
  • Utiliser json_init, json_add_*, json_dump pour la sortie JSON
  • Lire les parametres depuis stdin avec read -r input et json_load
  • Toujours valider les parametres d'entree
  • Retourner un statut succes/erreur approprie
  • Implementer le cas list pour declarer toutes les methodes et parametres
  • Implementer le cas call pour router vers les gestionnaires de methodes

Modele JSON de menu

Fichier: 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 - Modules de securite et surveillance (CrowdSec, Auth Guardian)
  • monitoring - Modules de monitoring (Netdata)
  • network - Modules reseau (Netifyd, Network Modes, WireGuard)
  • system - Modules systeme (System Hub)
  • services - Modules de services (CDN Cache, VHost Manager)

Points cles:

  • Les chemins de menu suivent admin/secubox/<category>/<module-name>
  • La premiere entree utilise "type": "firstchild" pour rediriger vers le premier enfant
  • Les entrees suivantes utilisent "type": "view" avec "path" correspondant a l'emplacement du fichier de vue
  • Le chemin DOIT correspondre: "path": "<module-name>/overview" -> view/<module-name>/overview.js
  • L'ordre determine la position dans le menu (les nombres les plus bas en premier)
  • Depend de l'entree ACL correspondant au nom du paquet

Modele JSON ACL

Fichier: 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>"]
		}
	}
}

Points cles:

  • Le nom de l'entree ACL DOIT correspondre au nom du paquet
  • La section read liste les methodes pouvant etre appelees sans permissions d'ecriture
  • La section write liste les methodes qui modifient l'etat
  • Les noms d'objets ubus DOIVENT correspondre au nom du script RPCD (luci.<module-name>)
  • L'acces a la configuration UCI peut etre accorde separement
  • Tous les noms de methodes DOIVENT correspondre exactement a ceux definis dans le script RPCD

Modele de style CSS

Fichier: 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);
}

Points cles:

  • Toujours importer system-hub/common.css pour les variables du systeme de design
  • Utiliser les variables CSS (var(--sh-*)) au lieu de couleurs codees en dur
  • Supporter le mode sombre avec les selecteurs [data-theme="dark"]
  • Utiliser des mises en page en grille responsive (grid-template-columns: repeat(auto-fit, minmax(...)))
  • Ajouter des transitions fluides pour une meilleure UX
  • Utiliser JetBrains Mono pour les valeurs numeriques
  • Suivre le design responsive mobile-first

Exemple d'implementation complete

Voici un exemple minimal fonctionnel complet pour un nouveau module appele "Example Dashboard":

Structure des repertoires

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;
}

Etapes d'installation:

  1. Copier les fichiers dans le repertoire du module
  2. Definir les permissions RPCD: chmod +x root/usr/libexec/rpcd/luci.example-dashboard
  3. Valider: ./secubox-tools/validate-modules.sh
  4. Compiler: ./secubox-tools/local-build.sh build luci-app-example-dashboard
  5. Deployer: scp build/x86-64/*.ipk root@router:/tmp/
  6. Installer: ssh root@router "opkg install /tmp/luci-app-example-dashboard*.ipk && /etc/init.d/rpcd restart"

Pieges courants et solutions

1. Erreur RPCD "Object not found"

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

Solutions:

  • Verifier que le nom du script RPCD correspond exactement au nom de l'objet ubus (incluant le prefixe luci.)
  • Verifier que le script RPCD est executable: chmod +x root/usr/libexec/rpcd/luci.example-dashboard
  • Redemarrer le service RPCD: /etc/init.d/rpcd restart
  • Verifier les logs RPCD: logread | grep rpcd

2. Erreur HTTP 404 Vue non trouvee

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

Solutions:

  • Verifier que le chemin du menu correspond exactement a l'emplacement du fichier de vue
  • Menu: "path": "example-dashboard/overview" -> Fichier: view/example-dashboard/overview.js
  • Verifier les permissions des fichiers: 644 pour les fichiers CSS/JS
  • Vider le cache du navigateur

3. Permission ACL refusee

Erreur: Acces refuse ou permissions manquantes

Solutions:

  • Verifier que le fichier ACL existe dans root/usr/share/rpcd/acl.d/
  • Verifier que le nom de l'objet ubus dans l'ACL correspond au nom du script RPCD
  • Redemarrer RPCD: /etc/init.d/rpcd restart
  • Verifier que la methode est listee dans la section appropriee (read vs write)

4. CSS non charge

Probleme: Les styles ne sont pas appliques

Solutions:

  • Verifier le chemin d'import CSS: L.resource('example-dashboard/dashboard.css')
  • Verifier les permissions des fichiers: 644
  • Importer common.css: @import url('../system-hub/common.css');
  • Vider le cache du navigateur
  • Verifier la console du navigateur pour les erreurs 404

5. Rafraichissement automatique non fonctionnel

Probleme: Le poll ne met pas a jour le tableau de bord

Solutions:

  • Verifier que poll.add() est appele dans la methode render()
  • Verifier que les appels API dans le callback poll retournent des Promises
  • S'assurer que la methode updateDashboard() met correctement a jour le DOM
  • Utiliser la console du navigateur pour verifier les erreurs JavaScript

Liste de verification de validation

Avant de deployer, toujours executer ces verifications:

# 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

Version du document: 1.0.0 Derniere mise a jour: 2025-12-27 Mainteneur: CyberMind.fr