release: v0.2.2 - Design System v0.3.0 & Comprehensive Documentation
🎨 Design System v0.3.0 (Demo-inspired) - New dark palette: #0a0a0f, #6366f1→#8b5cf6 gradients - Typography: Inter + JetBrains Mono - Compact stats grid (130px min) - Gradient text effects with background-clip - Sticky navigation tabs - Enhanced card borders and hover effects 📚 Comprehensive Documentation Suite - DEVELOPMENT-GUIDELINES.md (33KB, 900+ lines) - 9 major sections: Design, Architecture, RPCD, ACL, JS, CSS, Errors, Validation, Deployment - Complete code templates and best practices - Common error diagnostics and solutions - QUICK-START.md (6.4KB) - 8 critical rules for immediate reference - Quick code templates - Error quick fixes table - deploy-module-template.sh (8.1KB) - Standardized deployment with automatic backup - Permission fixes, cache clearing, verification - Updated CLAUDE.md, README.md with documentation index - Updated .claude/README.md to v2.0 🔄 Version Updates - luci-app-secubox: 0.1.2 → 0.2.2 - luci-app-system-hub: 0.1.1 → 0.2.2 - Updated all version strings (api.js, overview.js, CSS files) 🎯 CSS Enhancements - common.css: Complete rewrite with demo palette - overview.css: Dashboard header with gradient - services.css: Updated version to 0.2.2 - components.css: Updated version to 0.2.2 🔧 Critical Rules Documented 1. RPCD naming: file = ubus object (luci. prefix) 2. Menu path = view file location 3. Permissions: 755 (RPCD), 644 (CSS/JS) 4. ALWAYS run validate-modules.sh 5. CSS variables only (no hardcode) 6. Dark mode mandatory 7. Typography: Inter + JetBrains Mono 8. Gradients: --sh-primary → --sh-primary-end 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4e2763190d
commit
8e53825ad5
@ -1,435 +1,438 @@
|
||||
# 📚 Documentation Claude pour SecuBox
|
||||
# 📚 Configuration Claude Code pour SecuBox
|
||||
|
||||
Ce répertoire contient la documentation et les guides pour travailler avec Claude Code sur le projet SecuBox.
|
||||
**Version:** 2.0.0
|
||||
**Date:** 2025-12-26
|
||||
**Projet:** SecuBox OpenWrt
|
||||
**Design System:** v0.3.0 (Demo-inspired)
|
||||
|
||||
Ce répertoire contient la configuration et les guides pour travailler avec Claude Code sur le projet SecuBox.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Fichiers Disponibles
|
||||
## 🚨 IMPORTANT: Nouvelle Documentation (v2.0)
|
||||
|
||||
### 1. `module-prompts.md`
|
||||
**Prompts d'implémentation pour les 14 modules de base**
|
||||
**La documentation a été entièrement restructurée et améliorée!**
|
||||
|
||||
Contient les spécifications complètes pour chaque module SecuBox existant:
|
||||
- SecuBox Hub, System Hub
|
||||
- CrowdSec, Netdata, Netifyd Dashboards
|
||||
- Network Modes, Client Guardian, Auth Guardian
|
||||
- WireGuard, Bandwidth Manager, Media Flow
|
||||
- VHost Manager, CDN Cache, Traffic Shaper
|
||||
### 📖 Guides Principaux (À la racine du projet)
|
||||
|
||||
**Usage**: Copie-colle le prompt du module que tu veux implémenter ou modifier.
|
||||
| Guide | Description | Quand l'utiliser |
|
||||
|-------|-------------|------------------|
|
||||
| **[DEVELOPMENT-GUIDELINES.md](../DEVELOPMENT-GUIDELINES.md)** | ⭐ **GUIDE COMPLET** (33KB, 900+ lignes)<br/>Design System, RPCD/ubus, ACL, JS, CSS, Debug, Validation, Deployment | AVANT toute modification de code |
|
||||
| **[QUICK-START.md](../QUICK-START.md)** | ⚡ **AIDE-MÉMOIRE** (6.4KB)<br/>Règles critiques, commandes rapides, templates de code | Référence quotidienne |
|
||||
| **[CLAUDE.md](../CLAUDE.md)** | 🏗️ **ARCHITECTURE** (17KB)<br/>Build OpenWrt, structure fichiers, CI/CD | Build et architecture |
|
||||
| **[README.md](../README.md)** | 📘 **VUE D'ENSEMBLE** (18KB)<br/>Présentation projet, modules, quick start | Introduction au projet |
|
||||
|
||||
### 2. `module-implementation-guide.md` ⭐ NOUVEAU
|
||||
**Template structuré pour créer de nouveaux modules**
|
||||
### 🔧 Scripts & Tools
|
||||
|
||||
Guide complet avec:
|
||||
- Template réutilisable pour tout nouveau module
|
||||
- Checklist de validation complète
|
||||
- Exemple détaillé: KSM Manager (gestion de clés + Nitrokey)
|
||||
- Workflow d'implémentation étape par étape
|
||||
- Spécifications techniques détaillées
|
||||
|
||||
**Usage**:
|
||||
1. Copie le template
|
||||
2. Remplis les sections pour ton module
|
||||
3. Soumets le prompt complet à Claude
|
||||
4. Valide avec `validate-modules.sh`
|
||||
|
||||
### 3. `settings.local.json`
|
||||
**Configuration locale de Claude Code**
|
||||
|
||||
Contient les paramètres de développement pour cette session.
|
||||
| Script | Description | Usage |
|
||||
|--------|-------------|-------|
|
||||
| **[deploy-module-template.sh](../deploy-module-template.sh)** | Script de déploiement standardisé avec backup | `./deploy-module-template.sh <module-name>` |
|
||||
| **validate-modules.sh** | Validation complète des modules | `./secubox-tools/validate-modules.sh` |
|
||||
| **local-build.sh** | Build local avec SDK OpenWrt | `./secubox-tools/local-build.sh build` |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start: Créer un Nouveau Module
|
||||
## ⚠️ Règles Critiques (À TOUJOURS Respecter)
|
||||
|
||||
### Étape 1: Préparation
|
||||
### 1. RPCD Script Naming
|
||||
```
|
||||
RÈGLE: Nom fichier RPCD = objet ubus (EXACT!)
|
||||
|
||||
✅ CORRECT:
|
||||
JavaScript: object: 'luci.system-hub'
|
||||
Fichier: root/usr/libexec/rpcd/luci.system-hub
|
||||
|
||||
❌ INCORRECT (cause -32000 error):
|
||||
Fichier: root/usr/libexec/rpcd/system-hub
|
||||
```
|
||||
|
||||
### 2. Menu Path Matching
|
||||
```
|
||||
RÈGLE: Path menu = fichier vue (EXACT!)
|
||||
|
||||
✅ CORRECT:
|
||||
Menu JSON: "path": "system-hub/overview"
|
||||
Fichier: view/system-hub/overview.js
|
||||
|
||||
❌ INCORRECT (cause 404 error):
|
||||
Menu: "system-hub/overview"
|
||||
File: view/systemhub/overview.js
|
||||
```
|
||||
|
||||
### 3. Permissions
|
||||
```bash
|
||||
# Assure-toi d'être dans le bon répertoire
|
||||
cd /home/reepost/CyberMindStudio/_files/secubox
|
||||
# RPCD scripts = exécutable
|
||||
chmod 755 root/usr/libexec/rpcd/luci.*
|
||||
|
||||
# Lis le guide d'implémentation
|
||||
cat .claude/module-implementation-guide.md
|
||||
# CSS/JS = lecture seule
|
||||
chmod 644 htdocs/**/*.{css,js}
|
||||
```
|
||||
|
||||
### Étape 2: Rédiger les Spécifications
|
||||
|
||||
Ouvre `module-implementation-guide.md` et copie le template. Remplis:
|
||||
|
||||
**Obligatoire**:
|
||||
- Nom du module
|
||||
- Catégorie (Security/Network/System/Performance/Services)
|
||||
- Description et cas d'utilisation
|
||||
- 3-5 fonctionnalités principales
|
||||
- Méthodes RPCD (minimum 5-8)
|
||||
- Configuration UCI
|
||||
- Views JavaScript (minimum 2-3)
|
||||
|
||||
**Recommandé**:
|
||||
- Dépendances système
|
||||
- Spécifications de parsing CLI
|
||||
- Gestion d'erreurs
|
||||
- Notes de sécurité
|
||||
|
||||
### Étape 3: Soumettre à Claude
|
||||
|
||||
```
|
||||
[Colle ton prompt basé sur le template]
|
||||
```
|
||||
|
||||
Claude générera:
|
||||
- ✅ Makefile
|
||||
- ✅ RPCD Backend
|
||||
- ✅ API Client
|
||||
- ✅ Views JavaScript
|
||||
- ✅ Menu JSON
|
||||
- ✅ ACL JSON
|
||||
- ✅ README.md
|
||||
|
||||
### Étape 4: Validation
|
||||
|
||||
### 4. Validation OBLIGATOIRE
|
||||
```bash
|
||||
# Validation automatique
|
||||
# TOUJOURS exécuter avant commit
|
||||
./secubox-tools/validate-modules.sh
|
||||
```
|
||||
|
||||
# Vérification syntaxe JavaScript
|
||||
node -c luci-app-{module}/htdocs/luci-static/resources/**/*.js
|
||||
### 5. CSS Variables (PAS de hardcode!)
|
||||
```css
|
||||
/* ✅ CORRECT */
|
||||
color: var(--sh-text-primary);
|
||||
|
||||
# Test RPCD sur router (si disponible)
|
||||
ubus call luci.{module} status
|
||||
/* ❌ INCORRECT */
|
||||
color: #fafafa;
|
||||
```
|
||||
|
||||
### 6. Dark Mode (Support OBLIGATOIRE)
|
||||
```css
|
||||
/* TOUJOURS fournir styles dark mode */
|
||||
[data-theme="dark"] .my-component {
|
||||
background: var(--sh-bg-card);
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Typographie
|
||||
```css
|
||||
/* Texte général */
|
||||
font-family: 'Inter', sans-serif;
|
||||
|
||||
/* Valeurs numériques, IDs, code */
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
```
|
||||
|
||||
### 8. Gradients
|
||||
```css
|
||||
/* Utiliser les variables pour dégradés */
|
||||
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 Exemples d'Utilisation
|
||||
## 🎨 Design System v0.3.0
|
||||
|
||||
### Exemple 1: Module Simple (Monitoring)
|
||||
Inspiré de: https://cybermind.fr/apps/system-hub/demo.html
|
||||
|
||||
```markdown
|
||||
## Nouveau Module SecuBox: System Monitor
|
||||
|
||||
**Nom**: luci-app-system-monitor
|
||||
**Catégorie**: System
|
||||
**Description**: Monitoring temps réel CPU, RAM, Disk, Network
|
||||
|
||||
### Fonctionnalités:
|
||||
1. Métriques système (CPU%, RAM%, Disk%, Temp)
|
||||
2. Graphiques temps réel (5min, 1h, 24h)
|
||||
3. Alertes configurables (seuils)
|
||||
4. Export données (CSV, JSON)
|
||||
|
||||
### Méthodes RPCD:
|
||||
- status
|
||||
- get_metrics
|
||||
- get_history
|
||||
- set_alert
|
||||
- list_alerts
|
||||
|
||||
[... reste du template ...]
|
||||
### Palette Dark Mode (Recommandé)
|
||||
```css
|
||||
--sh-bg-primary: #0a0a0f; /* Fond principal (noir profond) */
|
||||
--sh-bg-secondary: #12121a; /* Fond cartes/sections */
|
||||
--sh-bg-tertiary: #1a1a24; /* Fond hover/actif */
|
||||
--sh-border: #2a2a35; /* Bordures */
|
||||
--sh-primary: #6366f1; /* Indigo */
|
||||
--sh-primary-end: #8b5cf6; /* Violet (dégradés) */
|
||||
--sh-success: #22c55e; /* Vert */
|
||||
--sh-danger: #ef4444; /* Rouge */
|
||||
--sh-warning: #f59e0b; /* Orange */
|
||||
```
|
||||
|
||||
### Exemple 2: Module Complexe (KSM Manager)
|
||||
### Components CSS (Classes principales)
|
||||
```css
|
||||
.sh-page-header /* En-tête page avec gradient title */
|
||||
.sh-page-title /* Titre avec effet gradient text */
|
||||
.sh-stat-badge /* Badge stat (130px min) */
|
||||
.sh-card /* Carte avec bordure gradient hover */
|
||||
.sh-btn-primary /* Bouton gradient indigo-violet */
|
||||
.sh-filter-tab /* Onglet de filtre */
|
||||
.sh-nav-tab /* Onglet navigation sticky */
|
||||
```
|
||||
|
||||
Voir l'exemple complet dans `module-implementation-guide.md` → Section "Exemple Concret: Module KSM"
|
||||
### Grid Sizes
|
||||
```css
|
||||
/* Stats compacts */
|
||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||
|
||||
22 méthodes RPCD, 8 views, support HSM hardware, gestion certificats, audit logs, etc.
|
||||
/* Metrics moyens */
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
|
||||
### Exemple 3: Module Intégration (Home Assistant)
|
||||
|
||||
```markdown
|
||||
## Nouveau Module SecuBox: Home Assistant Bridge
|
||||
|
||||
**Nom**: luci-app-hass-bridge
|
||||
**Catégorie**: Services
|
||||
**Description**: Intégration bidirectionnelle avec Home Assistant
|
||||
|
||||
### Fonctionnalités:
|
||||
1. Auto-discovery MQTT
|
||||
2. Entities SecuBox → HASS (sensors, switches)
|
||||
3. Services HASS → SecuBox (actions)
|
||||
4. Webhooks bidirectionnels
|
||||
5. Dashboard widgets
|
||||
|
||||
### Méthodes RPCD:
|
||||
- status
|
||||
- get_entities
|
||||
- publish_entity
|
||||
- trigger_service
|
||||
- list_webhooks
|
||||
- add_webhook
|
||||
|
||||
[... reste du template ...]
|
||||
/* Info cards larges */
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Bonnes Pratiques
|
||||
## 🚀 Workflow de Développement
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
**Package**:
|
||||
- Format: `luci-app-{nom-module}` (tout en minuscules, tirets)
|
||||
- Exemples: `luci-app-cdn-cache`, `luci-app-ksm-manager`
|
||||
|
||||
**RPCD Script**:
|
||||
- **OBLIGATOIRE**: `luci.{nom-module}` (préfixe `luci.` requis!)
|
||||
- Emplacement: `/root/usr/libexec/rpcd/luci.{nom-module}`
|
||||
- Permissions: Exécutable (`chmod +x`)
|
||||
|
||||
**UCI Config**:
|
||||
- Fichier: `/etc/config/{nom-module}` (sans `luci-app-`)
|
||||
- Exemple: `/etc/config/ksm` pour `luci-app-ksm-manager`
|
||||
|
||||
**Views**:
|
||||
- Emplacement: `/htdocs/luci-static/resources/view/{module}/`
|
||||
- Fichiers: `overview.js`, `{feature}.js`
|
||||
|
||||
**API Client**:
|
||||
- Emplacement: `/htdocs/luci-static/resources/{module}/api.js`
|
||||
- Export: `L.Class.extend({ ... })`
|
||||
|
||||
### Structure Minimale
|
||||
|
||||
**Petit module** (< 5 méthodes):
|
||||
### 1. Avant de Commencer
|
||||
```bash
|
||||
# Lire les guides
|
||||
cat ../QUICK-START.md # Règles critiques
|
||||
cat ../DEVELOPMENT-GUIDELINES.md # Guide complet (section pertinente)
|
||||
```
|
||||
luci-app-{module}/
|
||||
├── Makefile
|
||||
├── README.md
|
||||
|
||||
### 2. Développement
|
||||
```bash
|
||||
# Modifier le code
|
||||
vim luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js
|
||||
|
||||
# Valider IMMÉDIATEMENT
|
||||
./secubox-tools/validate-modules.sh
|
||||
```
|
||||
|
||||
### 3. Test Local (Optionnel)
|
||||
```bash
|
||||
# Build local
|
||||
./secubox-tools/local-build.sh build luci-app-system-hub
|
||||
|
||||
# Vérifier .ipk
|
||||
ls -la build/x86-64/luci-app-system-hub*.ipk
|
||||
```
|
||||
|
||||
### 4. Déploiement
|
||||
```bash
|
||||
# Déployer sur routeur de test
|
||||
./deploy-module-template.sh system-hub
|
||||
|
||||
# Le script fait automatiquement:
|
||||
# - Backup avec timestamp
|
||||
# - Deploy JS, CSS, RPCD, menu, ACL
|
||||
# - Fix permissions
|
||||
# - Clear cache
|
||||
# - Restart services
|
||||
# - Vérification
|
||||
```
|
||||
|
||||
### 5. Test Navigateur
|
||||
```
|
||||
1. Ouvrir en MODE PRIVÉ (Ctrl+Shift+N)
|
||||
2. URL: https://192.168.8.191/cgi-bin/luci/admin/secubox/system/system-hub
|
||||
3. F12 Console: vérifier pas d'erreurs
|
||||
4. F12 Network: tous fichiers 200 OK
|
||||
5. Tester dark/light mode
|
||||
6. Tester responsive (mobile view)
|
||||
```
|
||||
|
||||
### 6. Commit
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: improve system-hub overview with demo styling"
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Checklist Pre-Commit
|
||||
|
||||
**OBLIGATOIRE avant chaque commit:**
|
||||
|
||||
- [ ] `./secubox-tools/validate-modules.sh` ✅ PASSED
|
||||
- [ ] RPCD name = ubus object name
|
||||
- [ ] Menu path = view file path
|
||||
- [ ] Permissions: 755 (RPCD), 644 (CSS/JS)
|
||||
- [ ] JSON files valides (jsonlint)
|
||||
- [ ] CSS: variables utilisées (pas hardcode)
|
||||
- [ ] CSS: dark mode supporté `[data-theme="dark"]`
|
||||
- [ ] JS: gestion d'erreur sur API calls
|
||||
- [ ] Version incrémentée dans Makefile (PKG_VERSION)
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Erreurs Communes & Solutions Rapides
|
||||
|
||||
| Erreur | Cause | Solution Rapide |
|
||||
|--------|-------|-----------------|
|
||||
| **-32000 Object not found** | RPCD name ≠ ubus object | Renommer: `mv rpcd/wrong-name rpcd/luci.correct-name` |
|
||||
| **404 View not found** | Menu path ≠ file location | Corriger path dans `menu.d/*.json` |
|
||||
| **403 Forbidden CSS** | Permissions incorrectes | `chmod 644 *.css` |
|
||||
| **[object HTMLButtonElement]** | Array imbriqué dans E() | Enlever wrapper: `E('div', {}, renderButtons())` |
|
||||
| **Styles pas à jour** | Cache navigateur | Mode privé + Ctrl+Shift+R |
|
||||
|
||||
**Pour diagnostics détaillés:** Voir [DEVELOPMENT-GUIDELINES.md - Common Errors](../DEVELOPMENT-GUIDELINES.md#common-errors--solutions)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Debug Commands
|
||||
|
||||
```bash
|
||||
# Vérifier ubus objects
|
||||
ssh root@192.168.8.191 "ubus list | grep luci.system-hub"
|
||||
|
||||
# Tester RPCD directement
|
||||
ssh root@192.168.8.191 "/usr/libexec/rpcd/luci.system-hub call getHealth"
|
||||
|
||||
# Vérifier fichiers déployés
|
||||
ssh root@192.168.8.191 "ls -la /www/luci-static/resources/view/system-hub/"
|
||||
|
||||
# Vérifier permissions
|
||||
ssh root@192.168.8.191 "ls -la /usr/libexec/rpcd/luci.system-hub"
|
||||
|
||||
# Logs système
|
||||
ssh root@192.168.8.191 "logread | grep -i error | tail -20"
|
||||
|
||||
# Clear cache + restart
|
||||
ssh root@192.168.8.191 "rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* && /etc/init.d/rpcd restart && /etc/init.d/uhttpd restart"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Fichiers de ce Répertoire (.claude/)
|
||||
|
||||
### Fichiers Actifs (v2.0)
|
||||
|
||||
| Fichier | Description | Status |
|
||||
|---------|-------------|--------|
|
||||
| **README.md** | Ce fichier - Guide configuration Claude | ✅ ACTIF v2.0 |
|
||||
| **settings.local.json** | Configuration locale Claude Code | ✅ ACTIF v2.0 |
|
||||
|
||||
### Fichiers Legacy (Deprecated)
|
||||
|
||||
| Fichier | Description | Migration |
|
||||
|---------|-------------|-----------|
|
||||
| `module-prompts.md` | Anciens prompts modules (18KB) | → DEVELOPMENT-GUIDELINES.md |
|
||||
| `module-implementation-guide.md` | Ancien template modules (23KB) | → DEVELOPMENT-GUIDELINES.md |
|
||||
| `context.md` | Ancien contexte (13KB) | → README.md + CLAUDE.md |
|
||||
|
||||
**⚠️ Les fichiers legacy sont conservés pour référence historique mais ne doivent plus être utilisés.**
|
||||
|
||||
**Utilisez maintenant:**
|
||||
- DEVELOPMENT-GUIDELINES.md (guide complet)
|
||||
- QUICK-START.md (aide-mémoire)
|
||||
- CLAUDE.md (architecture)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Templates de Code Rapides
|
||||
|
||||
### Page Header
|
||||
```javascript
|
||||
E('div', { 'class': 'sh-page-header' }, [
|
||||
E('div', {}, [
|
||||
E('h2', { 'class': 'sh-page-title' }, [
|
||||
E('span', { 'class': 'sh-page-title-icon' }, '🎯'),
|
||||
'Page Title'
|
||||
]),
|
||||
E('p', { 'class': 'sh-page-subtitle' }, 'Description')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stats-grid' }, [
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value' }, '92'),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Score')
|
||||
])
|
||||
])
|
||||
])
|
||||
```
|
||||
|
||||
### Card with Gradient Border
|
||||
```javascript
|
||||
E('div', { 'class': 'sh-card sh-card-success' }, [
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
E('h3', { 'class': 'sh-card-title' }, [
|
||||
E('span', { 'class': 'sh-card-title-icon' }, '⚙️'),
|
||||
'Card Title'
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-body' }, [
|
||||
// Content here
|
||||
])
|
||||
])
|
||||
```
|
||||
|
||||
### RPCD Script
|
||||
```bash
|
||||
#!/bin/sh
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"getStatus": {}, "getHealth": {}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
getStatus)
|
||||
printf '{"enabled": true, "version": "1.0.0"}\n'
|
||||
;;
|
||||
getHealth)
|
||||
cpu=$(top -bn1 | grep "CPU:" | awk '{print $2}' | sed 's/%//')
|
||||
printf '{"cpu": {"usage": %s}}\n' "$cpu"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
**Pour plus de templates:** Voir [QUICK-START.md - Quick Code Templates](../QUICK-START.md#quick-code-templates)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Structure d'un Module Type
|
||||
|
||||
```
|
||||
luci-app-<module-name>/
|
||||
├── Makefile # Package OpenWrt
|
||||
├── README.md # Documentation module
|
||||
├── htdocs/luci-static/resources/
|
||||
│ ├── {module}/
|
||||
│ │ └── api.js
|
||||
│ └── view/{module}/
|
||||
│ └── overview.js
|
||||
│ ├── view/<module-name>/
|
||||
│ │ ├── overview.js # Vue principale
|
||||
│ │ ├── settings.js # Configuration (optionnel)
|
||||
│ │ └── *.js # Autres vues
|
||||
│ └── <module-name>/
|
||||
│ ├── api.js # Client RPC
|
||||
│ ├── common.css # Styles partagés
|
||||
│ └── overview.css # Styles page
|
||||
└── root/
|
||||
├── usr/
|
||||
│ ├── libexec/rpcd/
|
||||
│ │ └── luci.{module}
|
||||
│ └── share/
|
||||
│ ├── luci/menu.d/
|
||||
│ │ └── luci-app-{module}.json
|
||||
│ └── rpcd/acl.d/
|
||||
│ └── luci-app-{module}.json
|
||||
└── etc/config/
|
||||
└── {module} (optionnel)
|
||||
```
|
||||
|
||||
**Module complet** (> 8 méthodes):
|
||||
```
|
||||
[Structure minimale +]
|
||||
├── htdocs/luci-static/resources/
|
||||
│ └── view/{module}/
|
||||
│ ├── overview.js
|
||||
│ ├── management.js
|
||||
│ ├── settings.js
|
||||
│ └── logs.js
|
||||
└── root/etc/init.d/
|
||||
└── {module} (si besoin d'un daemon)
|
||||
```
|
||||
|
||||
### Checklist Pré-Commit
|
||||
|
||||
Avant de commiter un nouveau module:
|
||||
|
||||
- [ ] `./secubox-tools/validate-modules.sh` passe ✅
|
||||
- [ ] Tous les fichiers JavaScript valident avec `node -c`
|
||||
- [ ] Tous les JSON valident avec `jsonlint`
|
||||
- [ ] RPCD script est exécutable
|
||||
- [ ] Nom RPCD = `luci.{module}` (avec préfixe!)
|
||||
- [ ] README.md complet avec installation/usage
|
||||
- [ ] Makefile a toutes les dépendances
|
||||
- [ ] ACL contient toutes les méthodes RPCD
|
||||
- [ ] Menu paths matchent les fichiers view
|
||||
- [ ] Git commit message descriptif
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Outils de Développement
|
||||
|
||||
### Validation Automatique
|
||||
|
||||
```bash
|
||||
# Validation complète de tous les modules
|
||||
./secubox-tools/validate-modules.sh
|
||||
|
||||
# Validation d'un module spécifique
|
||||
./secubox-tools/secubox-debug.sh luci-app-{module}
|
||||
|
||||
# Réparation automatique des problèmes courants
|
||||
./secubox-tools/secubox-repair.sh
|
||||
```
|
||||
|
||||
### Build Local
|
||||
|
||||
```bash
|
||||
# Build tous les packages
|
||||
./secubox-tools/local-build.sh build
|
||||
|
||||
# Build un package spécifique
|
||||
./secubox-tools/local-build.sh build luci-app-{module}
|
||||
|
||||
# Build pour architecture spécifique
|
||||
./secubox-tools/local-build.sh build --arch aarch64-cortex-a72
|
||||
|
||||
# Validation seule (rapide)
|
||||
./secubox-tools/local-build.sh validate
|
||||
```
|
||||
|
||||
### Test sur Router
|
||||
|
||||
```bash
|
||||
# Transfer IPK
|
||||
scp build/x86-64/luci-app-{module}_*.ipk root@192.168.1.1:/tmp/
|
||||
|
||||
# Install sur router
|
||||
ssh root@192.168.1.1
|
||||
opkg install /tmp/luci-app-{module}_*.ipk
|
||||
/etc/init.d/rpcd restart
|
||||
/etc/init.d/uhttpd restart
|
||||
|
||||
# Test RPC manuel
|
||||
ubus list | grep {module}
|
||||
ubus call luci.{module} status
|
||||
├── usr/libexec/rpcd/
|
||||
│ └── luci.<module-name> ⚠️ MUST match ubus object!
|
||||
└── usr/share/
|
||||
├── luci/menu.d/
|
||||
│ └── luci-app-<module-name>.json
|
||||
└── rpcd/acl.d/
|
||||
└── luci-app-<module-name>.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Ressources
|
||||
|
||||
### Documentation OpenWrt/LuCI
|
||||
|
||||
- [LuCI API Reference](https://openwrt.github.io/luci/)
|
||||
- [UCI Configuration](https://openwrt.org/docs/guide-user/base-system/uci)
|
||||
- [RPCD Guide](https://openwrt.org/docs/techref/rpcd)
|
||||
- [OpenWrt Packages](https://openwrt.org/packages/start)
|
||||
|
||||
### Exemples de Code
|
||||
|
||||
Tous les 14 modules SecuBox existants servent de référence:
|
||||
|
||||
**Simples** (bon pour débuter):
|
||||
- `luci-app-netdata-dashboard` - Iframe simple + contrôles
|
||||
- `luci-app-network-modes` - Preset application
|
||||
|
||||
**Moyens**:
|
||||
- `luci-app-bandwidth-manager` - QoS avec graphiques
|
||||
- `luci-app-media-flow` - Détection + stats
|
||||
|
||||
**Avancés** (patterns complexes):
|
||||
- `luci-app-wireguard-dashboard` - Génération clés + QR codes
|
||||
- `luci-app-auth-guardian` - OAuth + vouchers + sessions
|
||||
- `luci-app-traffic-shaper` - TC/CAKE intégration
|
||||
|
||||
### Architecture SecuBox
|
||||
## 🌐 URLs de Test
|
||||
|
||||
**System Hub (Design v0.3.0):**
|
||||
```
|
||||
SecuBox Hub (luci-app-secubox)
|
||||
├── Security Layer
|
||||
│ ├── CrowdSec Dashboard
|
||||
│ └── Auth Guardian
|
||||
├── Network Layer
|
||||
│ ├── Network Modes
|
||||
│ ├── Client Guardian
|
||||
│ ├── WireGuard Dashboard
|
||||
│ └── VHost Manager
|
||||
├── Traffic Layer
|
||||
│ ├── Bandwidth Manager
|
||||
│ ├── Media Flow
|
||||
│ ├── CDN Cache
|
||||
│ └── Traffic Shaper
|
||||
├── Monitoring Layer
|
||||
│ ├── Netdata Dashboard
|
||||
│ ├── Netifyd Dashboard
|
||||
│ └── System Hub
|
||||
└── [Nouveau Module]
|
||||
https://192.168.8.191/cgi-bin/luci/admin/secubox/system/system-hub
|
||||
```
|
||||
|
||||
**SecuBox Dashboard:**
|
||||
```
|
||||
https://192.168.8.191/cgi-bin/luci/admin/secubox
|
||||
```
|
||||
|
||||
**⚠️ TOUJOURS tester en mode privé/incognito** après déploiement!
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contribution
|
||||
## 📚 Liens Utiles
|
||||
|
||||
### Workflow Git
|
||||
|
||||
```bash
|
||||
# Créer branche pour nouveau module
|
||||
git checkout -b feature/luci-app-{module}
|
||||
|
||||
# Développer avec validation continue
|
||||
# ... développement ...
|
||||
./secubox-tools/validate-modules.sh
|
||||
|
||||
# Commit
|
||||
git add luci-app-{module}/
|
||||
git commit -m "feat: implement {Module Name} - {brief description}"
|
||||
|
||||
# Push
|
||||
git push origin feature/luci-app-{module}
|
||||
|
||||
# Tag pour release
|
||||
git tag v0.0.X
|
||||
git push origin v0.0.X
|
||||
```
|
||||
|
||||
### Format de Commit Messages
|
||||
|
||||
```
|
||||
feat: implement KSM Manager - hardware key storage with Nitrokey
|
||||
fix: correct RPCD method naming in CDN Cache
|
||||
docs: add installation guide for Traffic Shaper
|
||||
chore: update dependencies for Bandwidth Manager
|
||||
refactor: improve error handling in Auth Guardian
|
||||
```
|
||||
- **Démo design:** https://cybermind.fr/apps/system-hub/demo.html
|
||||
- **OpenWrt LuCI:** https://github.com/openwrt/luci
|
||||
- **OpenWrt Docs:** https://openwrt.org/docs/
|
||||
- **Issues Claude Code:** https://github.com/anthropics/claude-code/issues
|
||||
|
||||
---
|
||||
|
||||
## 💡 Support
|
||||
## 📝 Changelog Configuration
|
||||
|
||||
### Debug Module Issues
|
||||
### Version 2.0.0 (2025-12-26)
|
||||
|
||||
**Problème**: Module n'apparaît pas dans le menu
|
||||
- Vérifier menu JSON path
|
||||
- Vérifier ACL permissions
|
||||
- Redémarrer uhttpd: `/etc/init.d/uhttpd restart`
|
||||
**Ajouts majeurs:**
|
||||
- ✅ DEVELOPMENT-GUIDELINES.md (33KB, guide complet)
|
||||
- ✅ QUICK-START.md (6.4KB, aide-mémoire)
|
||||
- ✅ deploy-module-template.sh (script standardisé)
|
||||
- ✅ Design System v0.3.0 (demo-inspired)
|
||||
- ✅ 8 règles critiques documentées
|
||||
- ✅ Checklists validation complètes
|
||||
- ✅ Templates de code prêts à l'emploi
|
||||
|
||||
**Problème**: RPC errors "Object not found"
|
||||
- Vérifier nom RPCD = `luci.{module}`
|
||||
- Vérifier RPCD exécutable: `chmod +x`
|
||||
- Redémarrer rpcd: `/etc/init.d/rpcd restart`
|
||||
- Tester ubus: `ubus list | grep {module}`
|
||||
**Dépréciations:**
|
||||
- ⚠️ module-prompts.md → migré vers DEVELOPMENT-GUIDELINES.md
|
||||
- ⚠️ module-implementation-guide.md → migré vers DEVELOPMENT-GUIDELINES.md
|
||||
- ⚠️ context.md → migré vers README.md + CLAUDE.md
|
||||
|
||||
**Problème**: JavaScript errors
|
||||
- Valider syntaxe: `node -c {file}.js`
|
||||
- Vérifier imports: `'require {module}/api'`
|
||||
- Check console browser (F12)
|
||||
**Améliorations:**
|
||||
- ✅ Documentation structurée en 4 guides principaux
|
||||
- ✅ Workflow de développement clarifié
|
||||
- ✅ Erreurs communes documentées avec solutions
|
||||
- ✅ Script de déploiement avec backup automatique
|
||||
|
||||
**Problème**: Build failures
|
||||
- Vérifier Makefile dependencies
|
||||
- Vérifier include path: `../../luci.mk`
|
||||
- Clean build: `make clean`
|
||||
### Version 1.0.0 (2023-12-23)
|
||||
|
||||
### Demander de l'Aide
|
||||
|
||||
Ouvre une issue GitHub avec:
|
||||
1. Nom du module
|
||||
2. Description du problème
|
||||
3. Logs d'erreur
|
||||
4. Output de `./secubox-tools/validate-modules.sh`
|
||||
5. Configuration (anonymisée si nécessaire)
|
||||
- Version initiale avec module-prompts.md et module-implementation-guide.md
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Success Stories
|
||||
|
||||
Modules déjà implémentés avec succès:
|
||||
|
||||
1. **WireGuard Dashboard** - Génération peers + QR codes
|
||||
2. **Auth Guardian** - OAuth + vouchers complète
|
||||
3. **Bandwidth Manager** - QoS avec graphiques temps réel
|
||||
4. **Media Flow** - Détection streaming avec donut chart
|
||||
5. **CDN Cache** - Hit ratio gauge + cache management
|
||||
6. **Traffic Shaper** - TC/CAKE avec presets
|
||||
|
||||
Tous validés ✅ et production-ready 🚀
|
||||
|
||||
---
|
||||
|
||||
**Bon développement avec SecuBox!** 🔧🔐🌐
|
||||
**Dernière mise à jour:** 2025-12-26
|
||||
**Maintenu par:** CyberMind Studio
|
||||
**Version:** 2.0.0
|
||||
|
||||
@ -100,7 +100,21 @@
|
||||
"Bash(luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/*.js)",
|
||||
"Bash(luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub )",
|
||||
"Bash(luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json )",
|
||||
"Bash(LUCI_DEVELOPMENT_REFERENCE.md)"
|
||||
"Bash(LUCI_DEVELOPMENT_REFERENCE.md)",
|
||||
"Bash(while read file)",
|
||||
"Bash(do echo \"=== $file ===\" grep \"object:\" \"$file\")",
|
||||
"Bash(for file in luci-app-secubox/htdocs/luci-static/resources/view/secubox/*.js luci-app-secubox/htdocs/luci-static/resources/secubox/*.js)",
|
||||
"Bash(./deploy-secubox-fix.sh)",
|
||||
"Bash(./deploy-modules-v2.sh:*)",
|
||||
"Bash(./deploy-dynamic-modules.sh:*)",
|
||||
"Bash(/tmp/force-reload-luci.sh:*)",
|
||||
"Bash(/tmp/deploy-common-css.sh)",
|
||||
"Bash(/tmp/deploy-services.sh)",
|
||||
"Bash(/tmp/deploy-health.sh)",
|
||||
"Bash(/tmp/deploy-system-hub-all.sh)",
|
||||
"Bash(/tmp/deploy-secubox-final.sh)",
|
||||
"WebFetch(domain:cybermind.fr)",
|
||||
"Bash(/tmp/deploy-system-hub-demo-style.sh)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
39
CLAUDE.md
39
CLAUDE.md
@ -2,6 +2,45 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 📚 Documentation Index
|
||||
|
||||
**IMPORTANT:** Before working on any code, consult these guides:
|
||||
|
||||
1. **[DEVELOPMENT-GUIDELINES.md](./DEVELOPMENT-GUIDELINES.md)** - ⭐ **GUIDE COMPLET**
|
||||
- Design System & UI Guidelines (palettes, typographie, composants)
|
||||
- Architecture & Naming Conventions (RPCD, menu paths, prefixes)
|
||||
- RPCD & ubus Best Practices (erreurs communes, solutions)
|
||||
- ACL & Permissions (templates, validations)
|
||||
- JavaScript Patterns (API modules, views, event handling)
|
||||
- CSS/Styling Standards (variables, responsive, dark mode)
|
||||
- Common Errors & Solutions (diagnostics, fixes)
|
||||
- Validation Checklist (pre-commit, pre-deploy, post-deploy)
|
||||
- Deployment Procedures (scripts, rollback, versioning)
|
||||
|
||||
2. **[QUICK-START.md](./QUICK-START.md)** - ⚡ **AIDE-MÉMOIRE RAPIDE**
|
||||
- Règles critiques (RPCD naming, menu paths, permissions)
|
||||
- Design system essentials (couleurs, fonts, classes)
|
||||
- Common commands (validation, build, deploy, debug)
|
||||
- Quick code templates (RPCD, View, Headers, Cards)
|
||||
- Error quick fixes
|
||||
|
||||
3. **CLAUDE.md** (ce fichier) - 🏗️ **ARCHITECTURE & BUILD**
|
||||
- Build commands (OpenWrt SDK, local build)
|
||||
- Module structure (files, directories)
|
||||
- CI/CD workflows
|
||||
- Common issues techniques
|
||||
|
||||
**⚠️ RÈGLES CRITIQUES À TOUJOURS RESPECTER:**
|
||||
|
||||
1. **RPCD Script Naming:** Nom fichier = objet ubus (`luci.system-hub`)
|
||||
2. **Menu Path Matching:** Path menu = fichier vue (`system-hub/overview.js`)
|
||||
3. **Permissions:** RPCD = 755, CSS/JS = 644
|
||||
4. **Validation:** Toujours exécuter `./secubox-tools/validate-modules.sh` avant commit
|
||||
5. **CSS Variables:** Toujours utiliser `var(--sh-*)`, jamais hardcoder les couleurs
|
||||
6. **Dark Mode:** Toujours supporter dark mode avec `[data-theme="dark"]`
|
||||
7. **Typography:** Inter (texte), JetBrains Mono (valeurs numériques)
|
||||
8. **Gradient Effects:** Utiliser `--sh-primary` → `--sh-primary-end` pour dégradés
|
||||
|
||||
## Project Overview
|
||||
|
||||
SecuBox is a comprehensive security and network management suite for OpenWrt. The repository contains 13 LuCI application packages that provide dashboards for security monitoring, network intelligence, access control, bandwidth management, and system administration.
|
||||
|
||||
1443
DEVELOPMENT-GUIDELINES.md
Normal file
1443
DEVELOPMENT-GUIDELINES.md
Normal file
File diff suppressed because it is too large
Load Diff
284
QUICK-START.md
Normal file
284
QUICK-START.md
Normal file
@ -0,0 +1,284 @@
|
||||
# Quick Start Guide - SecuBox Development
|
||||
|
||||
**⚡ Aide-mémoire rapide pour développement**
|
||||
|
||||
Pour le guide complet, voir [DEVELOPMENT-GUIDELINES.md](./DEVELOPMENT-GUIDELINES.md)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ RÈGLES CRITIQUES (À NE JAMAIS OUBLIER)
|
||||
|
||||
### 1. RPCD Script Naming
|
||||
```bash
|
||||
# Le nom DOIT correspondre EXACTEMENT à l'objet ubus
|
||||
JavaScript: object: 'luci.system-hub'
|
||||
Fichier: root/usr/libexec/rpcd/luci.system-hub ✅
|
||||
|
||||
# Sinon: Error -32000 "Object not found"
|
||||
```
|
||||
|
||||
### 2. Menu Path Matching
|
||||
```json
|
||||
Menu JSON: "path": "system-hub/overview"
|
||||
Fichier: view/system-hub/overview.js ✅
|
||||
|
||||
# Sinon: HTTP 404 Not Found
|
||||
```
|
||||
|
||||
### 3. Permissions Files
|
||||
```bash
|
||||
# RPCD scripts = exécutable
|
||||
chmod 755 root/usr/libexec/rpcd/luci.*
|
||||
|
||||
# CSS/JS = lecture seule
|
||||
chmod 644 htdocs/**/*.{css,js}
|
||||
|
||||
# Sinon: 403 Forbidden ou script non exécuté
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Design System Essentials
|
||||
|
||||
### Color Palette (Dark Mode)
|
||||
```css
|
||||
--sh-bg-primary: #0a0a0f; /* Fond principal */
|
||||
--sh-bg-card: #12121a; /* Cartes */
|
||||
--sh-border: #2a2a35; /* Bordures */
|
||||
--sh-primary: #6366f1; /* Indigo */
|
||||
--sh-primary-end: #8b5cf6; /* Violet */
|
||||
```
|
||||
|
||||
### Fonts
|
||||
```css
|
||||
/* Général */
|
||||
font-family: 'Inter', sans-serif;
|
||||
|
||||
/* Valeurs numériques */
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
```
|
||||
|
||||
### Component Classes
|
||||
```css
|
||||
.sh-page-header /* Page header */
|
||||
.sh-page-title /* Title (gradient text) */
|
||||
.sh-stat-badge /* Stat badge (130px min) */
|
||||
.sh-card /* Card (gradient border on hover) */
|
||||
.sh-btn-primary /* Button (gradient) */
|
||||
.sh-filter-tab /* Filter tab */
|
||||
```
|
||||
|
||||
### Grid Sizes
|
||||
```css
|
||||
/* Stats */
|
||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||
|
||||
/* Metrics */
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
|
||||
/* Info cards */
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Common Commands
|
||||
|
||||
### Validation
|
||||
```bash
|
||||
# Valider TOUT avant commit
|
||||
./secubox-tools/validate-modules.sh
|
||||
|
||||
# JSON
|
||||
jsonlint file.json
|
||||
|
||||
# Shell
|
||||
shellcheck root/usr/libexec/rpcd/*
|
||||
```
|
||||
|
||||
### Build
|
||||
```bash
|
||||
# Build local
|
||||
./secubox-tools/local-build.sh build luci-app-module-name
|
||||
|
||||
# Build OpenWrt SDK
|
||||
make package/luci-app-module-name/compile V=s
|
||||
```
|
||||
|
||||
### Deploy
|
||||
```bash
|
||||
# Copier fichiers
|
||||
scp file.js root@192.168.8.191:/www/luci-static/resources/
|
||||
|
||||
# Fix permissions
|
||||
ssh root@192.168.8.191 "chmod 644 /www/luci-static/resources/**/*.css"
|
||||
|
||||
# Clear cache + restart
|
||||
ssh root@192.168.8.191 "rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* && /etc/init.d/rpcd restart && /etc/init.d/uhttpd restart"
|
||||
```
|
||||
|
||||
### Debug
|
||||
```bash
|
||||
# Test RPCD
|
||||
ssh root@router "ubus list | grep luci.module"
|
||||
ssh root@router "ubus call luci.module-name getStatus"
|
||||
|
||||
# Check files
|
||||
ssh root@router "ls -la /www/luci-static/resources/view/module-name/"
|
||||
|
||||
# Logs
|
||||
ssh root@router "logread | grep -i error"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Common Errors & Quick Fixes
|
||||
|
||||
| Error | Quick Fix |
|
||||
|-------|-----------|
|
||||
| **-32000 Object not found** | Rename RPCD file to match ubus object |
|
||||
| **404 View not found** | Fix menu path to match file location |
|
||||
| **403 Forbidden CSS** | `chmod 644 *.css` |
|
||||
| **[object HTMLButtonElement]** | Remove array wrapper: `E('div', {}, renderButtons())` |
|
||||
| **Styles not updating** | Clear browser cache (Ctrl+Shift+R) + mode privé |
|
||||
|
||||
---
|
||||
|
||||
## 📋 Pre-Commit Checklist
|
||||
|
||||
- [ ] `./secubox-tools/validate-modules.sh` ✅
|
||||
- [ ] RPCD name = ubus object name
|
||||
- [ ] Menu path = view file path
|
||||
- [ ] Permissions: 755 (RPCD), 644 (CSS/JS)
|
||||
- [ ] JSON valide (jsonlint)
|
||||
- [ ] CSS: variables utilisées (pas hardcode)
|
||||
- [ ] CSS: dark mode supporté
|
||||
- [ ] JS: gestion d'erreur sur API calls
|
||||
- [ ] Version incrémentée (PKG_VERSION)
|
||||
|
||||
---
|
||||
|
||||
## 📁 File Structure Template
|
||||
|
||||
```
|
||||
luci-app-<module>/
|
||||
├── Makefile
|
||||
├── htdocs/luci-static/resources/
|
||||
│ ├── view/<module>/
|
||||
│ │ └── overview.js
|
||||
│ └── <module>/
|
||||
│ ├── api.js
|
||||
│ ├── common.css
|
||||
│ └── overview.css
|
||||
└── root/
|
||||
├── usr/libexec/rpcd/
|
||||
│ └── luci.<module> ⚠️ MUST match ubus object!
|
||||
└── usr/share/
|
||||
├── luci/menu.d/
|
||||
│ └── luci-app-<module>.json
|
||||
└── rpcd/acl.d/
|
||||
└── luci-app-<module>.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Code Templates
|
||||
|
||||
### RPCD Script
|
||||
```bash
|
||||
#!/bin/sh
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"getStatus": {}, "getHealth": {}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
getStatus)
|
||||
printf '{"enabled": true}\n'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
### View (JavaScript)
|
||||
```javascript
|
||||
'use strict';
|
||||
'require view';
|
||||
'require <module>/api as API';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return API.getStatus();
|
||||
},
|
||||
render: function(data) {
|
||||
return E('div', { 'class': 'sh-page-header' }, [
|
||||
E('h2', { 'class': 'sh-page-title' }, 'Title')
|
||||
]);
|
||||
},
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
```
|
||||
|
||||
### Page Header
|
||||
```javascript
|
||||
E('div', { 'class': 'sh-page-header' }, [
|
||||
E('div', {}, [
|
||||
E('h2', { 'class': 'sh-page-title' }, [
|
||||
E('span', { 'class': 'sh-page-title-icon' }, '🎯'),
|
||||
'Page Title'
|
||||
]),
|
||||
E('p', { 'class': 'sh-page-subtitle' }, 'Description')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stats-grid' }, [
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value' }, '92'),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Score')
|
||||
])
|
||||
])
|
||||
])
|
||||
```
|
||||
|
||||
### Card with Gradient Border
|
||||
```javascript
|
||||
E('div', { 'class': 'sh-card sh-card-success' }, [
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
E('h3', { 'class': 'sh-card-title' }, [
|
||||
E('span', { 'class': 'sh-card-title-icon' }, '⚙️'),
|
||||
'Card Title'
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-body' }, [
|
||||
// Content
|
||||
])
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Test URLs
|
||||
|
||||
```
|
||||
SecuBox Dashboard:
|
||||
https://192.168.8.191/cgi-bin/luci/admin/secubox
|
||||
|
||||
System Hub:
|
||||
https://192.168.8.191/cgi-bin/luci/admin/secubox/system/system-hub
|
||||
```
|
||||
|
||||
**TOUJOURS tester en mode privé** (Ctrl+Shift+N) après deploy!
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- **Guide complet:** [DEVELOPMENT-GUIDELINES.md](./DEVELOPMENT-GUIDELINES.md)
|
||||
- **Architecture:** [CLAUDE.md](./CLAUDE.md)
|
||||
- **Validation:** `./secubox-tools/validate-modules.sh`
|
||||
- **Démo design:** https://cybermind.fr/apps/system-hub/demo.html
|
||||
|
||||
---
|
||||
|
||||
**Version:** 1.0.0 | **Date:** 2025-12-26
|
||||
24
README.md
24
README.md
@ -4,6 +4,30 @@
|
||||
[](https://github.com/gkerma/secubox/actions/workflows/test-validate.yml)
|
||||
[](LICENSE)
|
||||
|
||||
## 📚 Documentation pour Développeurs
|
||||
|
||||
**NOUVEAU (2025-12-26):** Guides complets de développement disponibles!
|
||||
|
||||
| Guide | Description | Public |
|
||||
|-------|-------------|--------|
|
||||
| **[DEVELOPMENT-GUIDELINES.md](./DEVELOPMENT-GUIDELINES.md)** | ⭐ Guide complet: Design System, RPCD/ubus, ACL, JavaScript, CSS, Debugging (100+ pages) | Développeurs, IA assistants |
|
||||
| **[QUICK-START.md](./QUICK-START.md)** | ⚡ Aide-mémoire rapide: Règles critiques, commandes, templates de code | Développeurs expérimentés |
|
||||
| **[CLAUDE.md](./CLAUDE.md)** | 🏗️ Architecture & Build: SDK OpenWrt, structure fichiers, CI/CD | Claude Code, automation |
|
||||
| **[deploy-module-template.sh](./deploy-module-template.sh)** | 🚀 Script de déploiement standardisé avec backup automatique | DevOps |
|
||||
|
||||
**⚠️ Règles Critiques:**
|
||||
1. RPCD naming: fichier = objet ubus (`luci.system-hub`)
|
||||
2. Menu paths: path menu = fichier vue (`system-hub/overview.js`)
|
||||
3. Permissions: RPCD=755, CSS/JS=644
|
||||
4. **TOUJOURS valider:** `./secubox-tools/validate-modules.sh`
|
||||
|
||||
**Design System (v0.3.0):** Inspiré de [demo Cybermind](https://cybermind.fr/apps/system-hub/demo.html)
|
||||
- Palette dark: `#0a0a0f` (fond), `#6366f1→#8b5cf6` (gradients)
|
||||
- Fonts: Inter (texte), JetBrains Mono (valeurs)
|
||||
- CSS classes: `.sh-*` (System Hub), `.sb-*` (SecuBox)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
SecuBox is a comprehensive security and network management suite for OpenWrt, providing a unified ecosystem of specialized dashboards and tools. All modules are compiled automatically for multiple OpenWrt architectures via GitHub Actions.
|
||||
|
||||
268
deploy-module-template.sh
Executable file
268
deploy-module-template.sh
Executable file
@ -0,0 +1,268 @@
|
||||
#!/bin/bash
|
||||
# Template de déploiement standardisé pour modules SecuBox/System Hub
|
||||
# Usage: ./deploy-module-template.sh <module-name>
|
||||
#
|
||||
# Exemple: ./deploy-module-template.sh system-hub
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# === Configuration ===
|
||||
ROUTER="${ROUTER:-root@192.168.8.191}"
|
||||
MODULE_NAME="${1}"
|
||||
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Chemins
|
||||
LOCAL_RESOURCES="$BASE_DIR/luci-app-$MODULE_NAME/htdocs/luci-static/resources"
|
||||
LOCAL_ROOT="$BASE_DIR/luci-app-$MODULE_NAME/root"
|
||||
REMOTE_RESOURCES="/www/luci-static/resources"
|
||||
REMOTE_ROOT=""
|
||||
|
||||
# Couleurs pour output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# === Functions ===
|
||||
|
||||
print_header() {
|
||||
echo ""
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${BLUE} 📦 Déploiement: $MODULE_NAME${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
print_step() {
|
||||
echo -e "${GREEN}▶${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
check_prerequisites() {
|
||||
print_step "Vérification des prérequis..."
|
||||
|
||||
# Vérifier que le module existe
|
||||
if [ ! -d "luci-app-$MODULE_NAME" ]; then
|
||||
print_error "Module luci-app-$MODULE_NAME non trouvé!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Vérifier connectivité routeur
|
||||
if ! ssh -o ConnectTimeout=5 "$ROUTER" "exit" 2>/dev/null; then
|
||||
print_error "Impossible de se connecter à $ROUTER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Prérequis OK"
|
||||
}
|
||||
|
||||
backup_remote() {
|
||||
print_step "Création backup sur le routeur..."
|
||||
|
||||
BACKUP_DIR="/root/luci-backups/$(date +%Y%m%d-%H%M%S)"
|
||||
ssh "$ROUTER" "mkdir -p $BACKUP_DIR"
|
||||
|
||||
# Backup view files
|
||||
if ssh "$ROUTER" "[ -d $REMOTE_RESOURCES/view/$MODULE_NAME ]" 2>/dev/null; then
|
||||
ssh "$ROUTER" "cp -r $REMOTE_RESOURCES/view/$MODULE_NAME $BACKUP_DIR/"
|
||||
fi
|
||||
|
||||
# Backup module files
|
||||
if ssh "$ROUTER" "[ -d $REMOTE_RESOURCES/$MODULE_NAME ]" 2>/dev/null; then
|
||||
ssh "$ROUTER" "cp -r $REMOTE_RESOURCES/$MODULE_NAME $BACKUP_DIR/"
|
||||
fi
|
||||
|
||||
# Backup RPCD
|
||||
if ssh "$ROUTER" "[ -f /usr/libexec/rpcd/luci.$MODULE_NAME ]" 2>/dev/null; then
|
||||
ssh "$ROUTER" "cp /usr/libexec/rpcd/luci.$MODULE_NAME $BACKUP_DIR/"
|
||||
fi
|
||||
|
||||
print_success "Backup créé: $BACKUP_DIR"
|
||||
}
|
||||
|
||||
deploy_js_files() {
|
||||
print_step "Déploiement fichiers JavaScript..."
|
||||
|
||||
if [ -d "$LOCAL_RESOURCES/view/$MODULE_NAME" ]; then
|
||||
ssh "$ROUTER" "mkdir -p $REMOTE_RESOURCES/view/$MODULE_NAME"
|
||||
scp -q "$LOCAL_RESOURCES/view/$MODULE_NAME/"*.js "$ROUTER:$REMOTE_RESOURCES/view/$MODULE_NAME/" 2>/dev/null || true
|
||||
print_success "Fichiers JS vues déployés"
|
||||
fi
|
||||
|
||||
if [ -f "$LOCAL_RESOURCES/$MODULE_NAME/api.js" ]; then
|
||||
ssh "$ROUTER" "mkdir -p $REMOTE_RESOURCES/$MODULE_NAME"
|
||||
scp -q "$LOCAL_RESOURCES/$MODULE_NAME/api.js" "$ROUTER:$REMOTE_RESOURCES/$MODULE_NAME/" 2>/dev/null || true
|
||||
print_success "API JS déployé"
|
||||
fi
|
||||
}
|
||||
|
||||
deploy_css_files() {
|
||||
print_step "Déploiement fichiers CSS..."
|
||||
|
||||
if [ -d "$LOCAL_RESOURCES/$MODULE_NAME" ]; then
|
||||
ssh "$ROUTER" "mkdir -p $REMOTE_RESOURCES/$MODULE_NAME"
|
||||
scp -q "$LOCAL_RESOURCES/$MODULE_NAME/"*.css "$ROUTER:$REMOTE_RESOURCES/$MODULE_NAME/" 2>/dev/null || true
|
||||
print_success "Fichiers CSS déployés"
|
||||
fi
|
||||
}
|
||||
|
||||
deploy_rpcd() {
|
||||
print_step "Déploiement backend RPCD..."
|
||||
|
||||
RPCD_FILE="$LOCAL_ROOT/usr/libexec/rpcd/luci.$MODULE_NAME"
|
||||
if [ -f "$RPCD_FILE" ]; then
|
||||
scp -q "$RPCD_FILE" "$ROUTER:/usr/libexec/rpcd/" 2>/dev/null || true
|
||||
print_success "RPCD backend déployé"
|
||||
else
|
||||
print_warning "Pas de backend RPCD trouvé"
|
||||
fi
|
||||
}
|
||||
|
||||
deploy_menu_acl() {
|
||||
print_step "Déploiement menu et ACL..."
|
||||
|
||||
# Menu
|
||||
MENU_FILE="$LOCAL_ROOT/usr/share/luci/menu.d/luci-app-$MODULE_NAME.json"
|
||||
if [ -f "$MENU_FILE" ]; then
|
||||
ssh "$ROUTER" "mkdir -p /usr/share/luci/menu.d"
|
||||
scp -q "$MENU_FILE" "$ROUTER:/usr/share/luci/menu.d/" 2>/dev/null || true
|
||||
print_success "Menu déployé"
|
||||
fi
|
||||
|
||||
# ACL
|
||||
ACL_FILE="$LOCAL_ROOT/usr/share/rpcd/acl.d/luci-app-$MODULE_NAME.json"
|
||||
if [ -f "$ACL_FILE" ]; then
|
||||
ssh "$ROUTER" "mkdir -p /usr/share/rpcd/acl.d"
|
||||
scp -q "$ACL_FILE" "$ROUTER:/usr/share/rpcd/acl.d/" 2>/dev/null || true
|
||||
print_success "ACL déployé"
|
||||
fi
|
||||
}
|
||||
|
||||
fix_permissions() {
|
||||
print_step "Correction des permissions..."
|
||||
|
||||
# RPCD = 755
|
||||
ssh "$ROUTER" "chmod 755 /usr/libexec/rpcd/luci.$MODULE_NAME 2>/dev/null" || true
|
||||
|
||||
# CSS/JS = 644
|
||||
ssh "$ROUTER" "chmod 644 $REMOTE_RESOURCES/$MODULE_NAME/*.css 2>/dev/null" || true
|
||||
ssh "$ROUTER" "chmod 644 $REMOTE_RESOURCES/$MODULE_NAME/*.js 2>/dev/null" || true
|
||||
ssh "$ROUTER" "chmod 644 $REMOTE_RESOURCES/view/$MODULE_NAME/*.js 2>/dev/null" || true
|
||||
|
||||
print_success "Permissions corrigées"
|
||||
}
|
||||
|
||||
clear_cache() {
|
||||
print_step "Nettoyage du cache LuCI..."
|
||||
|
||||
ssh "$ROUTER" "rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null" || true
|
||||
|
||||
print_success "Cache nettoyé"
|
||||
}
|
||||
|
||||
restart_services() {
|
||||
print_step "Redémarrage des services..."
|
||||
|
||||
ssh "$ROUTER" "/etc/init.d/rpcd restart" >/dev/null 2>&1
|
||||
sleep 1
|
||||
ssh "$ROUTER" "/etc/init.d/uhttpd restart" >/dev/null 2>&1
|
||||
sleep 1
|
||||
|
||||
print_success "Services redémarrés"
|
||||
}
|
||||
|
||||
verify_deployment() {
|
||||
print_step "Vérification du déploiement..."
|
||||
|
||||
# Vérifier ubus object
|
||||
if ssh "$ROUTER" "ubus list | grep -q luci.$MODULE_NAME" 2>/dev/null; then
|
||||
print_success "ubus object 'luci.$MODULE_NAME' détecté"
|
||||
else
|
||||
print_warning "ubus object 'luci.$MODULE_NAME' non trouvé"
|
||||
fi
|
||||
|
||||
# Vérifier fichiers
|
||||
FILE_COUNT=$(ssh "$ROUTER" "find $REMOTE_RESOURCES -name '*$MODULE_NAME*' -type f | wc -l" 2>/dev/null)
|
||||
print_success "$FILE_COUNT fichiers déployés"
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
echo ""
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${GREEN}✓ Déploiement terminé avec succès!${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}📋 Prochaines étapes:${NC}"
|
||||
echo ""
|
||||
echo "1. Tester en mode privé (Ctrl+Shift+N):"
|
||||
echo -e " ${BLUE}https://192.168.8.191/cgi-bin/luci/${NC}"
|
||||
echo ""
|
||||
echo "2. Vérifier console navigateur (F12):"
|
||||
echo " - Onglet Console: pas d'erreurs"
|
||||
echo " - Onglet Network: tous fichiers chargent (200)"
|
||||
echo ""
|
||||
echo "3. Tester fonctionnalités:"
|
||||
echo " - Navigation entre pages"
|
||||
echo " - Chargement des données"
|
||||
echo " - Actions (boutons, formulaires)"
|
||||
echo ""
|
||||
echo "4. Tester responsive:"
|
||||
echo " - Mode mobile (F12 > Toggle device toolbar)"
|
||||
echo " - Dark/Light mode"
|
||||
echo ""
|
||||
echo -e "${YELLOW}🔧 Debug (si problème):${NC}"
|
||||
echo ""
|
||||
echo "# Vérifier ubus"
|
||||
echo "ssh $ROUTER 'ubus list | grep $MODULE_NAME'"
|
||||
echo "ssh $ROUTER 'ubus call luci.$MODULE_NAME getStatus'"
|
||||
echo ""
|
||||
echo "# Vérifier logs"
|
||||
echo "ssh $ROUTER 'logread | grep -i error | tail -20'"
|
||||
echo ""
|
||||
echo "# Rollback (si nécessaire)"
|
||||
echo "ssh $ROUTER 'ls -la /root/luci-backups/'"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# === Main Execution ===
|
||||
|
||||
main() {
|
||||
# Vérifier argument
|
||||
if [ -z "$MODULE_NAME" ]; then
|
||||
echo "Usage: $0 <module-name>"
|
||||
echo ""
|
||||
echo "Exemples:"
|
||||
echo " $0 system-hub"
|
||||
echo " $0 secubox"
|
||||
echo " $0 netdata-dashboard"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_header
|
||||
check_prerequisites
|
||||
backup_remote
|
||||
deploy_js_files
|
||||
deploy_css_files
|
||||
deploy_rpcd
|
||||
deploy_menu_acl
|
||||
fix_permissions
|
||||
clear_cache
|
||||
restart_services
|
||||
verify_deployment
|
||||
print_summary
|
||||
}
|
||||
|
||||
# Run main
|
||||
main
|
||||
@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-secubox
|
||||
PKG_VERSION:=0.1.2
|
||||
PKG_VERSION:=0.2.2
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
|
||||
@ -263,7 +263,7 @@ return view.extend({
|
||||
'wireguard': 'admin/secubox/network/wireguard',
|
||||
'network_modes': 'admin/secubox/network/modes',
|
||||
'client_guardian': 'admin/secubox/security/guardian',
|
||||
'system_hub': 'admin/system/system-hub/overview',
|
||||
'system_hub': 'admin/secubox/system/system-hub',
|
||||
'bandwidth_manager': 'admin/secubox/network/bandwidth',
|
||||
'auth_guardian': 'admin/secubox/security/auth',
|
||||
'media_flow': 'admin/secubox/network/media',
|
||||
|
||||
@ -6,18 +6,6 @@
|
||||
'require secubox/theme as Theme';
|
||||
'require poll';
|
||||
|
||||
// Load CSS (base theme variables first)
|
||||
document.head.appendChild(E('link', {
|
||||
'rel': 'stylesheet',
|
||||
'type': 'text/css',
|
||||
'href': L.resource('secubox/secubox.css')
|
||||
}));
|
||||
document.head.appendChild(E('link', {
|
||||
'rel': 'stylesheet',
|
||||
'type': 'text/css',
|
||||
'href': L.resource('secubox/monitoring.css')
|
||||
}));
|
||||
|
||||
// Initialize theme
|
||||
Theme.init();
|
||||
|
||||
@ -87,7 +75,11 @@ return view.extend({
|
||||
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
var container = E('div', { 'class': 'secubox-monitoring-page' });
|
||||
var container = E('div', { 'class': 'secubox-monitoring-page' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/secubox.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/monitoring.css') })
|
||||
]);
|
||||
|
||||
// Header
|
||||
container.appendChild(this.renderHeader());
|
||||
@ -117,21 +109,49 @@ return view.extend({
|
||||
},
|
||||
|
||||
renderHeader: function() {
|
||||
return E('div', { 'class': 'secubox-page-header secubox-monitoring-header' }, [
|
||||
var latest = {
|
||||
cpu: this.cpuHistory[this.cpuHistory.length - 1] || { value: 0 },
|
||||
memory: this.memoryHistory[this.memoryHistory.length - 1] || { value: 0 },
|
||||
disk: this.diskHistory[this.diskHistory.length - 1] || { value: 0 }
|
||||
};
|
||||
|
||||
return E('div', { 'class': 'sh-page-header' }, [
|
||||
E('div', {}, [
|
||||
E('h2', {}, '📊 System Monitoring'),
|
||||
E('p', { 'class': 'secubox-page-subtitle' },
|
||||
E('h2', { 'class': 'sh-page-title' }, [
|
||||
E('span', { 'class': 'sh-page-title-icon' }, '📊'),
|
||||
'System Monitoring'
|
||||
]),
|
||||
E('p', { 'class': 'sh-page-subtitle' },
|
||||
'Real-time system performance metrics and historical trends')
|
||||
]),
|
||||
E('div', { 'class': 'secubox-header-info' }, [
|
||||
E('span', { 'class': 'secubox-badge' },
|
||||
'⏱️ Refresh: Every 5 seconds'),
|
||||
E('span', { 'class': 'secubox-badge' },
|
||||
'📈 History: Last ' + this.maxDataPoints + ' points')
|
||||
E('div', { 'class': 'sh-stats-grid' }, [
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value', 'style': 'color: ' + this.getColorForValue(latest.cpu.value) }, latest.cpu.value.toFixed(1) + '%'),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'CPU')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value', 'style': 'color: ' + this.getColorForValue(latest.memory.value) }, latest.memory.value.toFixed(1) + '%'),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Memory')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value', 'style': 'color: ' + this.getColorForValue(latest.disk.value) }, latest.disk.value.toFixed(1) + '%'),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Disk')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value' }, this.cpuHistory.length),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Data Points')
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
getColorForValue: function(value) {
|
||||
if (value >= 90) return '#ef4444';
|
||||
if (value >= 75) return '#f59e0b';
|
||||
if (value >= 50) return '#3b82f6';
|
||||
return '#22c55e';
|
||||
},
|
||||
|
||||
renderChart: function(type, title, unit) {
|
||||
return E('div', { 'class': 'secubox-chart-card' }, [
|
||||
E('h3', { 'class': 'secubox-chart-title' }, title),
|
||||
@ -353,6 +373,24 @@ return view.extend({
|
||||
if (container) {
|
||||
dom.content(container, this.renderStatsTable());
|
||||
}
|
||||
|
||||
// Update header stats
|
||||
var latest = {
|
||||
cpu: this.cpuHistory[this.cpuHistory.length - 1] || { value: 0 },
|
||||
memory: this.memoryHistory[this.memoryHistory.length - 1] || { value: 0 },
|
||||
disk: this.diskHistory[this.diskHistory.length - 1] || { value: 0 }
|
||||
};
|
||||
|
||||
var statBadges = document.querySelectorAll('.sh-stat-value');
|
||||
if (statBadges.length >= 4) {
|
||||
statBadges[0].textContent = latest.cpu.value.toFixed(1) + '%';
|
||||
statBadges[0].style.color = this.getColorForValue(latest.cpu.value);
|
||||
statBadges[1].textContent = latest.memory.value.toFixed(1) + '%';
|
||||
statBadges[1].style.color = this.getColorForValue(latest.memory.value);
|
||||
statBadges[2].textContent = latest.disk.value.toFixed(1) + '%';
|
||||
statBadges[2].style.color = this.getColorForValue(latest.disk.value);
|
||||
statBadges[3].textContent = this.cpuHistory.length;
|
||||
}
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
|
||||
@ -6,23 +6,55 @@
|
||||
'require secubox/api as API';
|
||||
'require secubox/theme as Theme';
|
||||
|
||||
// Initialize theme
|
||||
Theme.init();
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('secubox'),
|
||||
API.getStatus()
|
||||
API.getStatus(),
|
||||
Theme.getTheme()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var status = data[1] || {};
|
||||
var theme = data[2];
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('secubox', '⚙️ SecuBox Settings',
|
||||
'Configure global settings for the SecuBox security suite.');
|
||||
// Create wrapper container with modern header
|
||||
var container = E('div', { 'class': 'secubox-settings-page' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/secubox.css') }),
|
||||
|
||||
// Modern header
|
||||
E('div', { 'class': 'sh-page-header' }, [
|
||||
E('div', {}, [
|
||||
E('h2', { 'class': 'sh-page-title' }, [
|
||||
E('span', { 'class': 'sh-page-title-icon' }, '⚙️'),
|
||||
'SecuBox Settings'
|
||||
]),
|
||||
E('p', { 'class': 'sh-page-subtitle' },
|
||||
'Configure global settings for the SecuBox security suite')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stats-grid' }, [
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value' }, status.version || 'v0.1.2'),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Version')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value', 'style': status.enabled ? 'color: #22c55e;' : 'color: #ef4444;' },
|
||||
status.enabled ? 'ON' : 'OFF'),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Status')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value' }, status.modules_count || '14'),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Modules')
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
// Create form
|
||||
m = new form.Map('secubox', null, null);
|
||||
|
||||
// General Settings Section
|
||||
s = m.section(form.TypedSection, 'secubox', '🔧 General Settings');
|
||||
@ -37,7 +69,7 @@ return view.extend({
|
||||
o = s.option(form.Value, 'version', '📦 Version',
|
||||
'Current SecuBox version (read-only)');
|
||||
o.readonly = true;
|
||||
o.default = '0.1.0';
|
||||
o.default = '0.1.2';
|
||||
|
||||
// Dashboard Settings Section
|
||||
s = m.section(form.TypedSection, 'secubox', '📊 Dashboard Settings');
|
||||
@ -208,6 +240,10 @@ return view.extend({
|
||||
o.default = '20';
|
||||
o.placeholder = '20';
|
||||
|
||||
return m.render();
|
||||
// Render form and append to container
|
||||
return m.render().then(L.bind(function(formElement) {
|
||||
container.appendChild(formElement);
|
||||
return container;
|
||||
}, this));
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-system-hub
|
||||
PKG_VERSION:=0.1.1
|
||||
PKG_VERSION:=0.2.2
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
|
||||
@ -6,11 +6,11 @@
|
||||
* System Hub API
|
||||
* Package: luci-app-system-hub
|
||||
* RPCD object: luci.system-hub
|
||||
* Version: 0.1.1
|
||||
* Version: 0.2.2
|
||||
*/
|
||||
|
||||
// Debug log to verify correct version is loaded
|
||||
console.log('🔧 System Hub API v0.1.1 loaded at', new Date().toISOString());
|
||||
console.log('🔧 System Hub API v0.2.2 loaded at', new Date().toISOString());
|
||||
|
||||
var callStatus = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
|
||||
@ -0,0 +1,549 @@
|
||||
/**
|
||||
* System Hub - Common Styles (Demo-inspired)
|
||||
* Shared styles across all System Hub pages
|
||||
* Version: 0.3.0 - Matching https://cybermind.fr/apps/system-hub/demo.html
|
||||
*/
|
||||
|
||||
/* === Import Fonts === */
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
/* === Variables (Demo-inspired Dark Mode) === */
|
||||
:root {
|
||||
/* Light Mode (less used) */
|
||||
--sh-text-primary: #0f172a;
|
||||
--sh-text-secondary: #475569;
|
||||
--sh-bg-primary: #ffffff;
|
||||
--sh-bg-secondary: #f8fafc;
|
||||
--sh-bg-tertiary: #f1f5f9;
|
||||
--sh-bg-card: #ffffff;
|
||||
--sh-border: #e2e8f0;
|
||||
--sh-hover-bg: #f8fafc;
|
||||
--sh-hover-shadow: rgba(0, 0, 0, 0.1);
|
||||
--sh-primary: #6366f1;
|
||||
--sh-primary-end: #8b5cf6;
|
||||
--sh-shadow: rgba(0, 0, 0, 0.08);
|
||||
--sh-success: #22c55e;
|
||||
--sh-danger: #ef4444;
|
||||
--sh-warning: #f59e0b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Demo-inspired Dark Palette */
|
||||
--sh-text-primary: #fafafa;
|
||||
--sh-text-secondary: #a0a0b0;
|
||||
--sh-bg-primary: #0a0a0f;
|
||||
--sh-bg-secondary: #12121a;
|
||||
--sh-bg-tertiary: #1a1a24;
|
||||
--sh-bg-card: #12121a;
|
||||
--sh-border: #2a2a35;
|
||||
--sh-hover-bg: #1a1a24;
|
||||
--sh-hover-shadow: rgba(0, 0, 0, 0.6);
|
||||
--sh-primary: #6366f1;
|
||||
--sh-primary-end: #8b5cf6;
|
||||
--sh-shadow: rgba(0, 0, 0, 0.4);
|
||||
--sh-success: #22c55e;
|
||||
--sh-danger: #ef4444;
|
||||
--sh-warning: #f59e0b;
|
||||
}
|
||||
|
||||
/* === Global Typography === */
|
||||
body,
|
||||
.system-hub-dashboard,
|
||||
.sh-page-header,
|
||||
.sh-card {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
code,
|
||||
.sh-mono,
|
||||
.sh-id-display,
|
||||
pre {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* === Page Header === */
|
||||
.sh-page-header {
|
||||
margin-bottom: 24px;
|
||||
padding: 24px;
|
||||
background: var(--sh-bg-card);
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--sh-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.sh-page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sh-page-title-icon {
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
-webkit-text-fill-color: initial;
|
||||
}
|
||||
|
||||
.sh-page-subtitle {
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 14px;
|
||||
color: var(--sh-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* === Stats Badges (Compact Demo Style) === */
|
||||
.sh-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||
gap: 12px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.sh-stat-badge {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 12px;
|
||||
background: var(--sh-bg-card);
|
||||
border: 1px solid var(--sh-border);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sh-stat-badge::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-stat-badge:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px var(--sh-shadow);
|
||||
border-color: var(--sh-primary);
|
||||
}
|
||||
|
||||
.sh-stat-badge:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sh-stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-bottom: 6px;
|
||||
color: var(--sh-text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.sh-stat-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
color: var(--sh-text-secondary);
|
||||
}
|
||||
|
||||
/* === Navigation Tabs (Internal Demo Style) === */
|
||||
.sh-nav-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
background: var(--sh-bg-secondary);
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--sh-border);
|
||||
margin-bottom: 24px;
|
||||
overflow-x: auto;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.sh-nav-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 18px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--sh-text-secondary);
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sh-nav-tab:hover {
|
||||
background: var(--sh-hover-bg);
|
||||
color: var(--sh-text-primary);
|
||||
}
|
||||
|
||||
.sh-nav-tab.active {
|
||||
color: var(--sh-primary);
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.sh-nav-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, var(--sh-primary), var(--sh-primary-end));
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* === Filter Tabs === */
|
||||
.sh-filter-tabs {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
background: var(--sh-bg-secondary);
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--sh-border);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.sh-filter-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: var(--sh-bg-card);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--sh-text-secondary);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sh-filter-tab:hover {
|
||||
background: var(--sh-hover-bg);
|
||||
border-color: var(--sh-primary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.sh-filter-tab.active {
|
||||
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
|
||||
color: #ffffff;
|
||||
border-color: transparent;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.sh-tab-icon {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sh-tab-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* === Cards (with colored top border) === */
|
||||
.sh-card {
|
||||
background: var(--sh-bg-card);
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--sh-border);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sh-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-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 28px var(--sh-hover-shadow);
|
||||
border-color: var(--sh-border);
|
||||
}
|
||||
|
||||
.sh-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Colored borders for different card types */
|
||||
.sh-card-success::before {
|
||||
background: var(--sh-success);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sh-card-danger::before {
|
||||
background: var(--sh-danger);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sh-card-warning::before {
|
||||
background: var(--sh-warning);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sh-card-header {
|
||||
padding: 20px 24px;
|
||||
background: var(--sh-bg-secondary);
|
||||
border-bottom: 1px solid var(--sh-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sh-card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--sh-text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sh-card-title-icon {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sh-card-badge {
|
||||
padding: 6px 14px;
|
||||
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
|
||||
color: #ffffff;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sh-card-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* === Empty State === */
|
||||
.sh-empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background: var(--sh-bg-secondary);
|
||||
border-radius: 12px;
|
||||
border: 2px dashed var(--sh-border);
|
||||
}
|
||||
|
||||
.sh-empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.sh-empty-text {
|
||||
font-size: 16px;
|
||||
color: var(--sh-text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* === Buttons (Gradient Style) === */
|
||||
.sh-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sh-btn-primary {
|
||||
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.sh-btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.5);
|
||||
}
|
||||
|
||||
.sh-btn-success {
|
||||
background: var(--sh-success);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.sh-btn-success:hover {
|
||||
background: #16a34a;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
|
||||
.sh-btn-danger {
|
||||
background: var(--sh-danger);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.sh-btn-danger:hover {
|
||||
background: #dc2626;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
.sh-btn-warning {
|
||||
background: var(--sh-warning);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.sh-btn-warning:hover {
|
||||
background: #d97706;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(245, 158, 11, 0.5);
|
||||
}
|
||||
|
||||
.sh-btn-secondary {
|
||||
background: var(--sh-bg-tertiary);
|
||||
color: var(--sh-text-primary);
|
||||
border: 1px solid var(--sh-border);
|
||||
}
|
||||
|
||||
.sh-btn-secondary:hover {
|
||||
background: var(--sh-hover-bg);
|
||||
border-color: var(--sh-primary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* === ID Display (Monospace) === */
|
||||
.sh-id-display {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: var(--sh-text-primary);
|
||||
background: var(--sh-bg-secondary);
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--sh-border);
|
||||
text-align: center;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* === Gradient Text Utility === */
|
||||
.sh-gradient-text {
|
||||
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* === Responsive === */
|
||||
@media (max-width: 768px) {
|
||||
.sh-page-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.sh-filter-tabs {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sh-filter-tab {
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sh-stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.sh-nav-tabs {
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.sh-nav-tab {
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
/* === Dark Mode Overrides === */
|
||||
[data-theme="dark"] .sh-card,
|
||||
[data-theme="dark"] .sh-stat-badge,
|
||||
[data-theme="dark"] .sh-filter-tabs,
|
||||
[data-theme="dark"] .sh-nav-tabs,
|
||||
[data-theme="dark"] .sh-page-header {
|
||||
background: var(--sh-bg-card);
|
||||
border-color: var(--sh-border);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-card-header,
|
||||
[data-theme="dark"] .sh-filter-tabs,
|
||||
[data-theme="dark"] .sh-nav-tabs {
|
||||
background: var(--sh-bg-secondary);
|
||||
border-color: var(--sh-border);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-filter-tab,
|
||||
[data-theme="dark"] .sh-nav-tab {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-filter-tab:hover,
|
||||
[data-theme="dark"] .sh-nav-tab:hover {
|
||||
background: var(--sh-hover-bg);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-btn-secondary {
|
||||
background: var(--sh-bg-tertiary);
|
||||
border-color: var(--sh-border);
|
||||
color: var(--sh-text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-btn-secondary:hover {
|
||||
background: var(--sh-hover-bg);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-empty-state {
|
||||
background: var(--sh-bg-secondary);
|
||||
border-color: var(--sh-border);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-id-display {
|
||||
background: var(--sh-bg-secondary);
|
||||
border-color: var(--sh-border);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* System Hub - Components Page Styles
|
||||
* Responsive card layout with theme support
|
||||
* Version: 0.1.2
|
||||
* Version: 0.2.2
|
||||
*/
|
||||
|
||||
/* === Header & Filters === */
|
||||
|
||||
@ -1,52 +1,186 @@
|
||||
/**
|
||||
* System Hub - Overview Page Styles
|
||||
* System Hub - Overview Page Styles (Demo-inspired)
|
||||
* Modern dashboard with widgets and metrics
|
||||
* Version: 0.2.0
|
||||
* Version: 0.3.0 - Matching https://cybermind.fr/apps/system-hub/demo.html
|
||||
*/
|
||||
|
||||
/* === Header === */
|
||||
/* === Dashboard Header (Demo Style - more subtle) === */
|
||||
.sh-dashboard-header {
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%);
|
||||
border-radius: 16px;
|
||||
padding: 28px;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid var(--sh-border);
|
||||
color: var(--sh-text-primary);
|
||||
}
|
||||
|
||||
.sh-dashboard-header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sh-dashboard-header h2 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.sh-dashboard-subtitle {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--sh-text-secondary);
|
||||
}
|
||||
|
||||
.sh-dashboard-header-info {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sh-dashboard-badge {
|
||||
background: var(--sh-bg-card);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--sh-border);
|
||||
white-space: nowrap;
|
||||
color: var(--sh-text-primary);
|
||||
}
|
||||
|
||||
.sh-dashboard-badge-version {
|
||||
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
|
||||
color: #ffffff;
|
||||
border-color: transparent;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* === Stats Overview Grid (Demo Style - Compact) === */
|
||||
.sh-stats-overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.sh-stat-overview-card {
|
||||
background: var(--sh-bg-card);
|
||||
border-radius: 12px;
|
||||
padding: 20px 16px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--sh-border);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sh-stat-overview-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.sh-stat-overview-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 28px var(--sh-shadow);
|
||||
border-color: var(--sh-primary);
|
||||
}
|
||||
|
||||
.sh-stat-excellent::before { background: #22c55e; opacity: 1; }
|
||||
.sh-stat-good::before { background: #3b82f6; opacity: 1; }
|
||||
.sh-stat-warning::before { background: #f59e0b; opacity: 1; }
|
||||
.sh-stat-critical::before { background: #ef4444; opacity: 1; }
|
||||
.sh-stat-cpu::before { background: linear-gradient(90deg, #6366f1, #8b5cf6); opacity: 1; }
|
||||
.sh-stat-memory::before { background: linear-gradient(90deg, #8b5cf6, #ec4899); opacity: 1; }
|
||||
.sh-stat-disk::before { background: linear-gradient(90deg, #ec4899, #f43f5e); opacity: 1; }
|
||||
|
||||
.sh-stat-overview-icon {
|
||||
font-size: 36px;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sh-stat-overview-value {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-bottom: 10px;
|
||||
color: var(--sh-text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.sh-stat-overview-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
color: var(--sh-text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sh-stat-overview-status {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--sh-text-secondary);
|
||||
}
|
||||
|
||||
/* === Old Header (deprecated, keep for compatibility) === */
|
||||
.sh-overview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
padding: 24px;
|
||||
background: var(--sh-bg-card, #ffffff);
|
||||
background: var(--sh-bg-card);
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--sh-border, #e2e8f0);
|
||||
border: 1px solid var(--sh-border);
|
||||
}
|
||||
|
||||
.sh-overview-title h2 {
|
||||
font-size: 32px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--sh-text-primary, #1e293b);
|
||||
color: var(--sh-text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sh-title-icon {
|
||||
font-size: 36px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.sh-overview-subtitle {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: var(--sh-text-secondary, #64748b);
|
||||
font-size: 14px;
|
||||
color: var(--sh-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* === Health Score Circle === */
|
||||
.sh-score-circle {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 6px solid;
|
||||
border: 5px solid;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@ -71,34 +205,35 @@
|
||||
}
|
||||
|
||||
.sh-score-value {
|
||||
font-size: 42px;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: var(--sh-text-primary, #1e293b);
|
||||
color: var(--sh-text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.sh-score-label {
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--sh-text-secondary, #64748b);
|
||||
color: var(--sh-text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* === Metrics Grid === */
|
||||
.sh-metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.sh-metric-card {
|
||||
background: var(--sh-bg-card, #ffffff);
|
||||
background: var(--sh-bg-card);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--sh-border, #e2e8f0);
|
||||
border: 1px solid var(--sh-border);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@ -110,15 +245,15 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #6366f1, #8b5cf6);
|
||||
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(-4px);
|
||||
box-shadow: 0 12px 28px var(--sh-hover-shadow, rgba(0, 0, 0, 0.12));
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 28px var(--sh-hover-shadow);
|
||||
}
|
||||
|
||||
.sh-metric-card:hover::before {
|
||||
@ -133,74 +268,93 @@
|
||||
}
|
||||
|
||||
.sh-metric-icon {
|
||||
font-size: 32px;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sh-metric-title {
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--sh-text-primary, #1e293b);
|
||||
color: var(--sh-text-primary);
|
||||
}
|
||||
|
||||
.sh-metric-value {
|
||||
font-size: 48px;
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-bottom: 16px;
|
||||
color: var(--sh-text-primary, #1e293b);
|
||||
color: var(--sh-text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.sh-metric-progress {
|
||||
height: 8px;
|
||||
background: var(--sh-bg-tertiary, #f1f5f9);
|
||||
border-radius: 4px;
|
||||
height: 6px;
|
||||
background: var(--sh-bg-tertiary);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sh-metric-progress-bar {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
border-radius: 3px;
|
||||
transition: width 0.5s ease;
|
||||
background: linear-gradient(90deg, var(--sh-primary), var(--sh-primary-end));
|
||||
}
|
||||
|
||||
.sh-metric-details {
|
||||
font-size: 14px;
|
||||
color: var(--sh-text-secondary, #64748b);
|
||||
font-size: 13px;
|
||||
color: var(--sh-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* === Info Grid === */
|
||||
.sh-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.sh-info-card {
|
||||
background: var(--sh-bg-card, #ffffff);
|
||||
background: var(--sh-bg-card);
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--sh-border, #e2e8f0);
|
||||
border: 1px solid var(--sh-border);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sh-info-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-info-card:hover {
|
||||
box-shadow: 0 4px 12px var(--sh-hover-shadow, rgba(0, 0, 0, 0.08));
|
||||
box-shadow: 0 8px 20px var(--sh-hover-shadow);
|
||||
}
|
||||
|
||||
.sh-info-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sh-info-card-header {
|
||||
padding: 20px 24px;
|
||||
background: var(--sh-bg-secondary, #f8fafc);
|
||||
border-bottom: 1px solid var(--sh-border, #e2e8f0);
|
||||
padding: 18px 24px;
|
||||
background: var(--sh-bg-secondary);
|
||||
border-bottom: 1px solid var(--sh-border);
|
||||
}
|
||||
|
||||
.sh-info-card-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--sh-text-primary, #1e293b);
|
||||
color: var(--sh-text-primary);
|
||||
}
|
||||
|
||||
.sh-info-card-body {
|
||||
@ -215,7 +369,7 @@
|
||||
|
||||
.sh-info-row {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr auto;
|
||||
grid-template-columns: 36px 1fr auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
@ -224,24 +378,24 @@
|
||||
}
|
||||
|
||||
.sh-info-row:hover {
|
||||
background: var(--sh-hover-bg, #f8fafc);
|
||||
background: var(--sh-hover-bg);
|
||||
}
|
||||
|
||||
.sh-info-icon {
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sh-info-label {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--sh-text-secondary, #64748b);
|
||||
color: var(--sh-text-secondary);
|
||||
}
|
||||
|
||||
.sh-info-value {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--sh-text-primary, #1e293b);
|
||||
color: var(--sh-text-primary);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@ -250,24 +404,24 @@
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.sh-status-ok {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #16a34a;
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.sh-status-error {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #dc2626;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.sh-status-warning {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #d97706;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
/* === Link Button === */
|
||||
@ -276,18 +430,19 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: var(--sh-primary, #6366f1);
|
||||
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
|
||||
color: #ffffff;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.sh-link-button:hover {
|
||||
background: #4f46e5;
|
||||
transform: translateX(4px);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.5);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@ -312,32 +467,35 @@
|
||||
}
|
||||
|
||||
.sh-metric-value {
|
||||
font-size: 36px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.sh-stats-overview-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.sh-dashboard-header h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
/* === Dark Mode === */
|
||||
[data-theme="dark"] {
|
||||
--sh-text-primary: #f1f5f9;
|
||||
--sh-text-secondary: #cbd5e1;
|
||||
--sh-bg-primary: #0f172a;
|
||||
--sh-bg-secondary: #1e293b;
|
||||
--sh-bg-tertiary: #334155;
|
||||
--sh-bg-card: #1e293b;
|
||||
--sh-border: #334155;
|
||||
--sh-hover-bg: #334155;
|
||||
--sh-hover-shadow: rgba(0, 0, 0, 0.4);
|
||||
[data-theme="dark"] .sh-dashboard-header {
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
|
||||
border-color: var(--sh-border);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-metric-card,
|
||||
[data-theme="dark"] .sh-info-card,
|
||||
[data-theme="dark"] .sh-overview-header {
|
||||
[data-theme="dark"] .sh-overview-header,
|
||||
[data-theme="dark"] .sh-stat-overview-card,
|
||||
[data-theme="dark"] .sh-dashboard-badge {
|
||||
background: var(--sh-bg-card);
|
||||
border-color: var(--sh-border);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-info-card-header {
|
||||
background: var(--sh-bg-tertiary);
|
||||
background: var(--sh-bg-secondary);
|
||||
border-color: var(--sh-border);
|
||||
}
|
||||
|
||||
@ -350,17 +508,17 @@
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-score-excellent {
|
||||
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(34, 197, 94, 0.1));
|
||||
background: linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(34, 197, 94, 0.08));
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-score-good {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0.1));
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(59, 130, 246, 0.08));
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-score-warning {
|
||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.2), rgba(245, 158, 11, 0.1));
|
||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.15), rgba(245, 158, 11, 0.08));
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-score-critical {
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(239, 68, 68, 0.1));
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(239, 68, 68, 0.08));
|
||||
}
|
||||
|
||||
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* System Hub - Services Page Styles
|
||||
* Modern card-based service management interface
|
||||
* Version: 0.2.2
|
||||
*/
|
||||
|
||||
/* === Services Grid === */
|
||||
.sh-services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* === Service Cards === */
|
||||
.sh-services-grid .sh-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sh-services-grid .sh-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sh-services-grid .sh-card-title {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sh-services-grid .sh-card-badge {
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* === Service Status Badges === */
|
||||
.sh-services-grid .sh-status-badge {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.sh-services-grid .sh-status-ok {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.sh-services-grid .sh-status-error {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
/* === Action Buttons === */
|
||||
.sh-services-grid .sh-btn {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
/* === Responsive === */
|
||||
@media (max-width: 768px) {
|
||||
.sh-services-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sh-services-grid .sh-card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sh-services-grid .sh-card-badge {
|
||||
margin-left: 0;
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
/* === Dark Mode === */
|
||||
[data-theme="dark"] .sh-services-grid .sh-status-ok {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sh-services-grid .sh-status-error {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
}
|
||||
@ -2,136 +2,340 @@
|
||||
'require view';
|
||||
'require dom';
|
||||
'require ui';
|
||||
'require poll';
|
||||
'require system-hub/api as API';
|
||||
'require system-hub/theme as Theme';
|
||||
|
||||
// Load CSS
|
||||
document.head.appendChild(E('link', {
|
||||
'rel': 'stylesheet',
|
||||
'type': 'text/css',
|
||||
'href': L.resource('system-hub/dashboard.css')
|
||||
}));
|
||||
|
||||
// Initialize theme
|
||||
Theme.init();
|
||||
|
||||
// Helper: Get health status info based on score
|
||||
function getHealthStatus(score) {
|
||||
if (score >= 90) return { status: 'excellent', label: 'Excellent', color: '#22c55e' };
|
||||
if (score >= 75) return { status: 'good', label: 'Bon', color: '#3b82f6' };
|
||||
if (score >= 50) return { status: 'warning', label: 'Attention', color: '#f59e0b' };
|
||||
return { status: 'critical', label: 'Critique', color: '#ef4444' };
|
||||
}
|
||||
|
||||
// Helper: Format bytes to human-readable size
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes) return '0 B';
|
||||
var k = 1024;
|
||||
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
healthData: null,
|
||||
|
||||
load: function() {
|
||||
return API.getHealth();
|
||||
return Promise.all([
|
||||
API.getHealth(),
|
||||
Theme.getTheme()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var health = data;
|
||||
var healthInfo = getHealthStatus(health.score || 0);
|
||||
var self = this;
|
||||
this.healthData = data[0] || {};
|
||||
var theme = data[1];
|
||||
|
||||
var view = E('div', { 'class': 'system-hub-dashboard' }, [
|
||||
var container = E('div', { 'class': 'system-hub-health' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
|
||||
|
||||
// Global Score
|
||||
E('div', { 'class': 'sh-card' }, [
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '💚'), 'Score de Santé Global' ]),
|
||||
E('div', { 'class': 'sh-card-badge' }, (health.score || 0) + '/100')
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-body', 'style': 'text-align: center; padding: 40px;' }, [
|
||||
E('div', {
|
||||
'class': 'sh-score-circle ' + healthInfo.status,
|
||||
'style': 'width: 120px; height: 120px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px; font-size: 40px; font-weight: 800;'
|
||||
}, (health.score || 0).toString()),
|
||||
E('div', { 'style': 'font-size: 20px; font-weight: 700; margin-bottom: 8px;' }, healthInfo.label),
|
||||
E('div', { 'style': 'color: #707080;' }, 'Dernière vérification : ' + (health.timestamp || 'N/A'))
|
||||
])
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/overview.css') }),
|
||||
|
||||
// Header with health score
|
||||
this.renderHeader(),
|
||||
|
||||
// System Metrics Grid
|
||||
E('div', { 'class': 'sh-metrics-grid' }, [
|
||||
this.renderMetricCard('CPU', this.healthData.cpu),
|
||||
this.renderMetricCard('Memory', this.healthData.memory),
|
||||
this.renderMetricCard('Disk', this.healthData.disk),
|
||||
this.renderMetricCard('Temperature', this.healthData.temperature)
|
||||
]),
|
||||
|
||||
// Detailed Metrics
|
||||
E('div', { 'class': 'sh-card' }, [
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '📊'), 'Métriques Détaillées' ])
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-body' }, [
|
||||
E('div', { 'class': 'sh-health-grid' }, [
|
||||
this.renderDetailedMetric('🔲', 'CPU', health.cpu?.usage || 0, health.cpu?.status, 'Load: ' + (health.cpu?.load_1m || 'N/A')),
|
||||
this.renderDetailedMetric('💾', 'Mémoire', health.memory?.usage || 0, health.memory?.status, formatBytes((health.memory?.used_kb || 0) * 1024) + ' utilisés'),
|
||||
this.renderDetailedMetric('💿', 'Stockage', health.disk?.usage || 0, health.disk?.status, formatBytes((health.disk?.used_kb || 0) * 1024) + ' utilisés'),
|
||||
this.renderDetailedMetric('🌡️', 'Température', health.temperature?.value || 0, health.temperature?.status, 'Zone 0: CPU'),
|
||||
this.renderDetailedMetric('🌐', 'Réseau WAN', health.network?.wan_up ? 100 : 0, health.network?.status, health.network?.wan_up ? 'Connecté' : 'Déconnecté'),
|
||||
this.renderDetailedMetric('⚙️', 'Services', ((health.services?.running || 0) / ((health.services?.running || 0) + (health.services?.failed || 0)) * 100) || 0,
|
||||
health.services?.failed > 0 ? 'warning' : 'ok',
|
||||
(health.services?.running || 0) + '/' + ((health.services?.running || 0) + (health.services?.failed || 0)) + ' actifs')
|
||||
])
|
||||
])
|
||||
|
||||
// Network & Services Info
|
||||
E('div', { 'class': 'sh-info-grid' }, [
|
||||
this.renderNetworkCard(),
|
||||
this.renderServicesCard()
|
||||
]),
|
||||
|
||||
|
||||
// Recommendations
|
||||
health.recommendations && health.recommendations.length > 0 ? E('div', { 'class': 'sh-card' }, [
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '💡'), 'Recommandations' ])
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-body' },
|
||||
health.recommendations.map(function(rec) {
|
||||
return E('div', { 'style': 'display: flex; gap: 12px; align-items: flex-start; padding: 14px; background: rgba(245, 158, 11, 0.1); border-radius: 10px; border-left: 3px solid #f59e0b; margin-bottom: 10px;' }, [
|
||||
E('span', { 'style': 'font-size: 24px;' }, '⚠️'),
|
||||
E('div', {}, rec)
|
||||
]);
|
||||
})
|
||||
)
|
||||
]) : E('span'),
|
||||
|
||||
this.healthData.recommendations && this.healthData.recommendations.length > 0
|
||||
? this.renderRecommendationsCard()
|
||||
: E('div'),
|
||||
|
||||
// Actions
|
||||
E('div', { 'class': 'sh-btn-group' }, [
|
||||
E('button', {
|
||||
'class': 'sh-btn sh-btn-primary',
|
||||
'click': L.bind(this.generateReport, this)
|
||||
}, [ '📋 Générer Rapport' ]),
|
||||
E('button', { 'class': 'sh-btn' }, [ '📧 Envoyer par Email' ]),
|
||||
E('button', { 'class': 'sh-btn' }, [ '📥 Télécharger PDF' ])
|
||||
])
|
||||
this.renderActionsCard()
|
||||
]);
|
||||
|
||||
return view;
|
||||
// Setup auto-refresh
|
||||
poll.add(L.bind(function() {
|
||||
return API.getHealth().then(L.bind(function(refreshData) {
|
||||
this.healthData = refreshData || {};
|
||||
this.updateDashboard();
|
||||
}, this));
|
||||
}, this), 30);
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
renderDetailedMetric: function(icon, label, value, status, detail) {
|
||||
return E('div', { 'class': 'sh-health-metric' }, [
|
||||
E('div', { 'class': 'sh-metric-header' }, [
|
||||
E('div', { 'class': 'sh-metric-title' }, [ E('span', { 'class': 'sh-metric-icon' }, icon), label ]),
|
||||
E('div', { 'class': 'sh-metric-value ' + (status || 'ok') }, value + (label === 'Température' ? '°C' : '%'))
|
||||
renderHeader: function() {
|
||||
var score = this.healthData.score || 0;
|
||||
var scoreClass = score >= 80 ? 'excellent' : (score >= 60 ? 'good' : (score >= 40 ? 'warning' : 'critical'));
|
||||
var scoreLabel = score >= 80 ? 'Excellent' : (score >= 60 ? 'Bon' : (score >= 40 ? 'Attention' : 'Critique'));
|
||||
|
||||
return E('div', { 'class': 'sh-overview-header' }, [
|
||||
E('div', { 'class': 'sh-overview-title' }, [
|
||||
E('h2', {}, [
|
||||
E('span', { 'class': 'sh-title-icon' }, '💚'),
|
||||
' Health Monitor'
|
||||
]),
|
||||
E('p', { 'class': 'sh-overview-subtitle' },
|
||||
'Surveillance en temps réel de la santé du système')
|
||||
]),
|
||||
E('div', { 'class': 'sh-progress-bar' }, [
|
||||
E('div', { 'class': 'sh-progress-fill ' + (status || 'ok'), 'style': 'width: ' + Math.min(value, 100) + '%' })
|
||||
]),
|
||||
E('div', { 'style': 'font-size: 10px; color: #707080; margin-top: 8px;' }, detail)
|
||||
E('div', { 'class': 'sh-overview-score' }, [
|
||||
E('div', { 'class': 'sh-score-circle sh-score-' + scoreClass }, [
|
||||
E('div', { 'class': 'sh-score-value' }, score),
|
||||
E('div', { 'class': 'sh-score-label' }, scoreLabel)
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderMetricCard: function(type, data) {
|
||||
if (!data) return E('div');
|
||||
|
||||
var config = this.getMetricConfig(type, data);
|
||||
|
||||
return E('div', { 'class': 'sh-metric-card sh-metric-' + config.status }, [
|
||||
E('div', { 'class': 'sh-metric-header' }, [
|
||||
E('span', { 'class': 'sh-metric-icon' }, config.icon),
|
||||
E('span', { 'class': 'sh-metric-title' }, config.title)
|
||||
]),
|
||||
E('div', { 'class': 'sh-metric-value' }, config.value),
|
||||
E('div', { 'class': 'sh-metric-progress' }, [
|
||||
E('div', {
|
||||
'class': 'sh-metric-progress-bar',
|
||||
'style': 'width: ' + config.percentage + '%; background: ' + config.color
|
||||
})
|
||||
]),
|
||||
E('div', { 'class': 'sh-metric-details' }, config.details)
|
||||
]);
|
||||
},
|
||||
|
||||
getMetricConfig: function(type, data) {
|
||||
switch(type) {
|
||||
case 'CPU':
|
||||
return {
|
||||
icon: '🔥',
|
||||
title: 'CPU Usage',
|
||||
value: (data.usage || 0) + '%',
|
||||
percentage: data.usage || 0,
|
||||
status: data.status || 'ok',
|
||||
color: this.getStatusColor(data.usage || 0),
|
||||
details: 'Load: ' + (data.load_1m || '0') + ' • ' + (data.cores || 0) + ' cores'
|
||||
};
|
||||
case 'Memory':
|
||||
var usedMB = ((data.used_kb || 0) / 1024).toFixed(0);
|
||||
var totalMB = ((data.total_kb || 0) / 1024).toFixed(0);
|
||||
return {
|
||||
icon: '💾',
|
||||
title: 'Memory',
|
||||
value: (data.usage || 0) + '%',
|
||||
percentage: data.usage || 0,
|
||||
status: data.status || 'ok',
|
||||
color: this.getStatusColor(data.usage || 0),
|
||||
details: usedMB + ' MB / ' + totalMB + ' MB used'
|
||||
};
|
||||
case 'Disk':
|
||||
var usedGB = ((data.used_kb || 0) / 1024 / 1024).toFixed(1);
|
||||
var totalGB = ((data.total_kb || 0) / 1024 / 1024).toFixed(1);
|
||||
return {
|
||||
icon: '💿',
|
||||
title: 'Disk Space',
|
||||
value: (data.usage || 0) + '%',
|
||||
percentage: data.usage || 0,
|
||||
status: data.status || 'ok',
|
||||
color: this.getStatusColor(data.usage || 0),
|
||||
details: usedGB + ' GB / ' + totalGB + ' GB used'
|
||||
};
|
||||
case 'Temperature':
|
||||
return {
|
||||
icon: '🌡️',
|
||||
title: 'Temperature',
|
||||
value: (data.value || 0) + '°C',
|
||||
percentage: Math.min((data.value || 0), 100),
|
||||
status: data.status || 'ok',
|
||||
color: this.getTempColor(data.value || 0),
|
||||
details: 'Status: ' + (data.status || 'unknown')
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: '📊',
|
||||
title: type,
|
||||
value: 'N/A',
|
||||
percentage: 0,
|
||||
status: 'unknown',
|
||||
color: '#64748b',
|
||||
details: 'No data'
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
getStatusColor: function(usage) {
|
||||
if (usage >= 90) return '#ef4444';
|
||||
if (usage >= 75) return '#f59e0b';
|
||||
if (usage >= 50) return '#3b82f6';
|
||||
return '#22c55e';
|
||||
},
|
||||
|
||||
getTempColor: function(temp) {
|
||||
if (temp >= 80) return '#ef4444';
|
||||
if (temp >= 70) return '#f59e0b';
|
||||
if (temp >= 60) return '#3b82f6';
|
||||
return '#22c55e';
|
||||
},
|
||||
|
||||
renderNetworkCard: function() {
|
||||
var wan_status = this.healthData.network ? this.healthData.network.wan_up : false;
|
||||
|
||||
return E('div', { 'class': 'sh-info-card' }, [
|
||||
E('div', { 'class': 'sh-info-card-header' }, [
|
||||
E('h3', {}, [
|
||||
E('span', { 'style': 'margin-right: 8px;' }, '🌐'),
|
||||
'Network Status'
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sh-info-card-body' }, [
|
||||
E('div', { 'class': 'sh-info-list' }, [
|
||||
this.renderInfoRow('📡', 'WAN Connection',
|
||||
E('span', {
|
||||
'class': 'sh-status-badge sh-status-' + (wan_status ? 'ok' : 'error')
|
||||
}, wan_status ? '✓ Connected' : '✗ Disconnected')
|
||||
),
|
||||
this.renderInfoRow('🔌', 'Network Mode',
|
||||
this.healthData.network ? this.healthData.network.status : 'unknown'
|
||||
)
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderServicesCard: function() {
|
||||
var running = this.healthData.services ? this.healthData.services.running : 0;
|
||||
var failed = this.healthData.services ? this.healthData.services.failed : 0;
|
||||
var total = running + failed;
|
||||
|
||||
return E('div', { 'class': 'sh-info-card' }, [
|
||||
E('div', { 'class': 'sh-info-card-header' }, [
|
||||
E('h3', {}, [
|
||||
E('span', { 'style': 'margin-right: 8px;' }, '⚙️'),
|
||||
'System Services'
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sh-info-card-body' }, [
|
||||
E('div', { 'class': 'sh-info-list' }, [
|
||||
this.renderInfoRow('▶️', 'Running',
|
||||
E('span', { 'class': 'sh-status-badge sh-status-ok' }, running + ' services')
|
||||
),
|
||||
this.renderInfoRow('⏹️', 'Failed',
|
||||
failed > 0
|
||||
? E('span', { 'class': 'sh-status-badge sh-status-error' }, failed + ' services')
|
||||
: E('span', { 'class': 'sh-status-badge sh-status-ok' }, 'None')
|
||||
),
|
||||
this.renderInfoRow('📊', 'Health Rate',
|
||||
total > 0 ? ((running / total * 100).toFixed(0) + '%') : 'N/A'
|
||||
)
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderRecommendationsCard: function() {
|
||||
return E('div', { 'class': 'sh-card' }, [
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
E('h3', { 'class': 'sh-card-title' }, [
|
||||
E('span', { 'class': 'sh-card-title-icon' }, '💡'),
|
||||
'Recommendations'
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-badge', 'style': 'background: #f59e0b;' },
|
||||
this.healthData.recommendations.length)
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-body' },
|
||||
this.healthData.recommendations.map(function(rec) {
|
||||
return E('div', {
|
||||
'style': 'display: flex; gap: 12px; align-items: flex-start; padding: 14px; background: rgba(245, 158, 11, 0.1); border-radius: 10px; border-left: 3px solid #f59e0b; margin-bottom: 10px;'
|
||||
}, [
|
||||
E('span', { 'style': 'font-size: 24px;' }, '⚠️'),
|
||||
E('div', { 'style': 'flex: 1; color: var(--sh-text-primary);' }, rec)
|
||||
]);
|
||||
})
|
||||
)
|
||||
]);
|
||||
},
|
||||
|
||||
renderActionsCard: function() {
|
||||
return E('div', { 'class': 'sh-card' }, [
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
E('h3', { 'class': 'sh-card-title' }, [
|
||||
E('span', { 'class': 'sh-card-title-icon' }, '🛠️'),
|
||||
'Actions'
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-body' }, [
|
||||
E('div', { 'style': 'display: flex; gap: 12px; flex-wrap: wrap;' }, [
|
||||
E('button', {
|
||||
'class': 'sh-btn sh-btn-primary',
|
||||
'click': L.bind(this.generateReport, this)
|
||||
}, [
|
||||
E('span', {}, '📋'),
|
||||
E('span', {}, 'Generate Report')
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'sh-btn sh-btn-secondary',
|
||||
'click': function() {
|
||||
ui.addNotification(null, E('p', '⚠️ Email feature coming soon'), 'info');
|
||||
}
|
||||
}, [
|
||||
E('span', {}, '📧'),
|
||||
E('span', {}, 'Send Email')
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'sh-btn sh-btn-secondary',
|
||||
'click': function() {
|
||||
ui.addNotification(null, E('p', '⚠️ PDF export coming soon'), 'info');
|
||||
}
|
||||
}, [
|
||||
E('span', {}, '📥'),
|
||||
E('span', {}, 'Download PDF')
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderInfoRow: function(icon, label, value) {
|
||||
return E('div', { 'class': 'sh-info-row' }, [
|
||||
E('span', { 'class': 'sh-info-icon' }, icon),
|
||||
E('span', { 'class': 'sh-info-label' }, label),
|
||||
E('span', { 'class': 'sh-info-value' }, value)
|
||||
]);
|
||||
},
|
||||
|
||||
updateDashboard: function() {
|
||||
var metricsGrid = document.querySelector('.sh-metrics-grid');
|
||||
if (metricsGrid) {
|
||||
dom.content(metricsGrid, [
|
||||
this.renderMetricCard('CPU', this.healthData.cpu),
|
||||
this.renderMetricCard('Memory', this.healthData.memory),
|
||||
this.renderMetricCard('Disk', this.healthData.disk),
|
||||
this.renderMetricCard('Temperature', this.healthData.temperature)
|
||||
]);
|
||||
}
|
||||
|
||||
// Update health score
|
||||
var scoreValue = document.querySelector('.sh-score-value');
|
||||
var scoreCircle = document.querySelector('.sh-score-circle');
|
||||
if (scoreValue && scoreCircle) {
|
||||
var score = this.healthData.score || 0;
|
||||
var scoreClass = score >= 80 ? 'excellent' : (score >= 60 ? 'good' : (score >= 40 ? 'warning' : 'critical'));
|
||||
scoreValue.textContent = score;
|
||||
scoreCircle.className = 'sh-score-circle sh-score-' + scoreClass;
|
||||
}
|
||||
},
|
||||
|
||||
generateReport: function() {
|
||||
ui.showModal(_('Génération Rapport'), [
|
||||
E('p', {}, 'Génération du rapport de santé...'),
|
||||
ui.showModal(_('Generating Report'), [
|
||||
E('p', {}, 'Generating health report...'),
|
||||
E('div', { 'class': 'spinning' })
|
||||
]);
|
||||
|
||||
// Stub: Report generation not yet implemented
|
||||
setTimeout(function() {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, '⚠️ Report generation feature coming soon'), 'info');
|
||||
ui.addNotification(null, E('p', '⚠️ Report generation feature coming soon'), 'info');
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
|
||||
@ -1,103 +1,274 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require dom';
|
||||
'require system-hub/api as API';
|
||||
'require system-hub/theme as Theme';
|
||||
|
||||
// Load CSS
|
||||
document.head.appendChild(E('link', {
|
||||
'rel': 'stylesheet',
|
||||
'type': 'text/css',
|
||||
'href': L.resource('system-hub/dashboard.css')
|
||||
}));
|
||||
return view.extend({
|
||||
logs: [],
|
||||
currentFilter: 'all',
|
||||
searchQuery: '',
|
||||
lineCount: 100,
|
||||
|
||||
// Initialize theme
|
||||
Theme.init();
|
||||
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
return API.getLogs(100, '');
|
||||
return Promise.all([
|
||||
API.getLogs(100, ''),
|
||||
Theme.getTheme()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(logs) {
|
||||
var v = E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, _('System Logs')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('View and filter system logs'))
|
||||
render: function(data) {
|
||||
this.logs = data[0] || [];
|
||||
var theme = data[1];
|
||||
|
||||
var container = E('div', { 'class': 'system-hub-logs' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
|
||||
|
||||
// Header
|
||||
this.renderHeader(),
|
||||
|
||||
// Controls
|
||||
this.renderControls(),
|
||||
|
||||
// Filter tabs
|
||||
this.renderFilterTabs(),
|
||||
|
||||
// Log viewer
|
||||
E('div', { 'class': 'sh-card' }, [
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
E('h3', { 'class': 'sh-card-title' }, [
|
||||
E('span', { 'class': 'sh-card-title-icon' }, '📟'),
|
||||
'Log Output'
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-badge' }, this.getFilteredLogs().length + ' lines')
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-body', 'style': 'padding: 0;' }, [
|
||||
E('div', { 'id': 'log-container' })
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
var section = E('div', { 'class': 'cbi-section' });
|
||||
|
||||
// Filter controls
|
||||
var controlsDiv = E('div', { 'style': 'margin-bottom: 15px; display: flex; gap: 10px; align-items: center;' });
|
||||
|
||||
var filterInput = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'placeholder': _('Filter logs...'),
|
||||
'style': 'flex: 1;'
|
||||
});
|
||||
|
||||
var linesSelect = E('select', { 'class': 'cbi-input-select' }, [
|
||||
E('option', { 'value': '50' }, '50 lines'),
|
||||
E('option', { 'value': '100', 'selected': '' }, '100 lines'),
|
||||
E('option', { 'value': '200' }, '200 lines'),
|
||||
E('option', { 'value': '500' }, '500 lines'),
|
||||
E('option', { 'value': '1000' }, '1000 lines')
|
||||
]);
|
||||
|
||||
var refreshBtn = E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': L.bind(function() {
|
||||
this.refreshLogs(filterInput.value, parseInt(linesSelect.value));
|
||||
}, this)
|
||||
}, _('Refresh'));
|
||||
|
||||
var clearBtn = E('button', {
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
'click': function() {
|
||||
filterInput.value = '';
|
||||
}
|
||||
}, _('Clear Filter'));
|
||||
|
||||
controlsDiv.appendChild(filterInput);
|
||||
controlsDiv.appendChild(linesSelect);
|
||||
controlsDiv.appendChild(refreshBtn);
|
||||
controlsDiv.appendChild(clearBtn);
|
||||
|
||||
section.appendChild(controlsDiv);
|
||||
|
||||
// Log display
|
||||
var logContainer = E('div', { 'id': 'log-container' });
|
||||
section.appendChild(logContainer);
|
||||
|
||||
// Initial render
|
||||
this.renderLogs(logContainer, logs);
|
||||
this.updateLogDisplay();
|
||||
|
||||
v.appendChild(section);
|
||||
|
||||
return v;
|
||||
return container;
|
||||
},
|
||||
|
||||
renderLogs: function(container, logs) {
|
||||
var logsText = logs.length > 0 ? logs.join('\n') : _('No logs available');
|
||||
renderHeader: function() {
|
||||
var stats = this.getLogStats();
|
||||
|
||||
L.dom.content(container, [
|
||||
E('pre', {
|
||||
'style': 'background: #000; color: #0f0; padding: 15px; overflow: auto; max-height: 600px; font-size: 11px; font-family: monospace; border-radius: 5px;'
|
||||
}, logsText)
|
||||
return E('div', { 'class': 'sh-page-header' }, [
|
||||
E('div', {}, [
|
||||
E('h2', { 'class': 'sh-page-title' }, [
|
||||
E('span', { 'class': 'sh-page-title-icon' }, '📋'),
|
||||
'System Logs'
|
||||
]),
|
||||
E('p', { 'class': 'sh-page-subtitle' },
|
||||
'View and filter system logs in real-time')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stats-grid' }, [
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value' }, stats.total),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Total Lines')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value', 'style': 'color: #ef4444;' }, stats.errors),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Errors')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value', 'style': 'color: #f59e0b;' }, stats.warnings),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Warnings')
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
refreshLogs: function(filter, lines) {
|
||||
renderControls: function() {
|
||||
var self = this;
|
||||
|
||||
return E('div', { 'style': 'display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap;' }, [
|
||||
// Search box
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'placeholder': '🔍 Search logs...',
|
||||
'style': 'flex: 1; min-width: 250px; padding: 12px 16px; border-radius: 8px; border: 1px solid var(--sh-border); background: var(--sh-bg-card); color: var(--sh-text-primary); font-size: 14px;',
|
||||
'input': function(ev) {
|
||||
self.searchQuery = ev.target.value.toLowerCase();
|
||||
self.updateLogDisplay();
|
||||
}
|
||||
}),
|
||||
// Line count selector
|
||||
E('select', {
|
||||
'class': 'cbi-input-select',
|
||||
'style': 'padding: 12px 16px; border-radius: 8px; border: 1px solid var(--sh-border); background: var(--sh-bg-card); color: var(--sh-text-primary); font-size: 14px;',
|
||||
'change': function(ev) {
|
||||
self.lineCount = parseInt(ev.target.value);
|
||||
self.refreshLogs();
|
||||
}
|
||||
}, [
|
||||
E('option', { 'value': '50' }, '50 lines'),
|
||||
E('option', { 'value': '100', 'selected': '' }, '100 lines'),
|
||||
E('option', { 'value': '200' }, '200 lines'),
|
||||
E('option', { 'value': '500' }, '500 lines'),
|
||||
E('option', { 'value': '1000' }, '1000 lines')
|
||||
]),
|
||||
// Refresh button
|
||||
E('button', {
|
||||
'class': 'sh-btn sh-btn-primary',
|
||||
'click': L.bind(this.refreshLogs, this)
|
||||
}, [
|
||||
E('span', {}, '🔄'),
|
||||
E('span', {}, 'Refresh')
|
||||
]),
|
||||
// Download button
|
||||
E('button', {
|
||||
'class': 'sh-btn sh-btn-secondary',
|
||||
'click': function() {
|
||||
self.downloadLogs();
|
||||
}
|
||||
}, [
|
||||
E('span', {}, '📥'),
|
||||
E('span', {}, 'Download')
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderFilterTabs: function() {
|
||||
var stats = this.getLogStats();
|
||||
|
||||
return E('div', { 'class': 'sh-filter-tabs' }, [
|
||||
this.createFilterTab('all', '📋', 'All Logs', stats.total),
|
||||
this.createFilterTab('error', '🔴', 'Errors', stats.errors),
|
||||
this.createFilterTab('warning', '🟡', 'Warnings', stats.warnings),
|
||||
this.createFilterTab('info', '🔵', 'Info', stats.info)
|
||||
]);
|
||||
},
|
||||
|
||||
createFilterTab: function(filter, icon, label, count) {
|
||||
var self = this;
|
||||
var isActive = this.currentFilter === filter;
|
||||
|
||||
return E('div', {
|
||||
'class': 'sh-filter-tab' + (isActive ? ' active' : ''),
|
||||
'click': function() {
|
||||
self.currentFilter = filter;
|
||||
self.updateFilterTabs();
|
||||
self.updateLogDisplay();
|
||||
}
|
||||
}, [
|
||||
E('span', { 'class': 'sh-tab-icon' }, icon),
|
||||
E('span', { 'class': 'sh-tab-label' }, label + ' (' + count + ')')
|
||||
]);
|
||||
},
|
||||
|
||||
updateFilterTabs: function() {
|
||||
var tabs = document.querySelectorAll('.sh-filter-tab');
|
||||
tabs.forEach(function(tab, index) {
|
||||
var filters = ['all', 'error', 'warning', 'info'];
|
||||
if (filters[index] === this.currentFilter) {
|
||||
tab.classList.add('active');
|
||||
} else {
|
||||
tab.classList.remove('active');
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
updateLogDisplay: function() {
|
||||
var container = document.getElementById('log-container');
|
||||
if (!container) return;
|
||||
|
||||
var filtered = this.getFilteredLogs();
|
||||
var logsText = filtered.length > 0 ? filtered.join('\n') : 'No logs available';
|
||||
|
||||
dom.content(container, [
|
||||
E('pre', {
|
||||
'style': 'background: var(--sh-bg-tertiary); color: var(--sh-text-primary); padding: 20px; overflow: auto; max-height: 600px; font-size: 12px; font-family: "Courier New", monospace; border-radius: 0; margin: 0; line-height: 1.5;'
|
||||
}, logsText)
|
||||
]);
|
||||
|
||||
// Update badge
|
||||
var badge = document.querySelector('.sh-card-badge');
|
||||
if (badge) {
|
||||
badge.textContent = filtered.length + ' lines';
|
||||
}
|
||||
},
|
||||
|
||||
getFilteredLogs: function() {
|
||||
return this.logs.filter(function(line) {
|
||||
var lineLower = line.toLowerCase();
|
||||
|
||||
// Apply filter
|
||||
var matchesFilter = true;
|
||||
switch (this.currentFilter) {
|
||||
case 'error':
|
||||
matchesFilter = lineLower.includes('error') || lineLower.includes('err') || lineLower.includes('fail');
|
||||
break;
|
||||
case 'warning':
|
||||
matchesFilter = lineLower.includes('warn') || lineLower.includes('warning');
|
||||
break;
|
||||
case 'info':
|
||||
matchesFilter = lineLower.includes('info') || lineLower.includes('notice');
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply search
|
||||
var matchesSearch = !this.searchQuery || lineLower.includes(this.searchQuery);
|
||||
|
||||
return matchesFilter && matchesSearch;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
getLogStats: function() {
|
||||
var stats = {
|
||||
total: this.logs.length,
|
||||
errors: 0,
|
||||
warnings: 0,
|
||||
info: 0
|
||||
};
|
||||
|
||||
this.logs.forEach(function(line) {
|
||||
var lineLower = line.toLowerCase();
|
||||
if (lineLower.includes('error') || lineLower.includes('err') || lineLower.includes('fail')) {
|
||||
stats.errors++;
|
||||
} else if (lineLower.includes('warn') || lineLower.includes('warning')) {
|
||||
stats.warnings++;
|
||||
} else if (lineLower.includes('info') || lineLower.includes('notice')) {
|
||||
stats.info++;
|
||||
}
|
||||
});
|
||||
|
||||
return stats;
|
||||
},
|
||||
|
||||
refreshLogs: function() {
|
||||
ui.showModal(_('Loading Logs'), [
|
||||
E('p', { 'class': 'spinning' }, _('Fetching logs...'))
|
||||
]);
|
||||
|
||||
API.getLogs(lines, filter).then(L.bind(function(logs) {
|
||||
API.getLogs(this.lineCount, '').then(L.bind(function(logs) {
|
||||
ui.hideModal();
|
||||
var container = document.getElementById('log-container');
|
||||
if (container) {
|
||||
this.renderLogs(container, logs);
|
||||
this.logs = logs || [];
|
||||
this.updateLogDisplay();
|
||||
|
||||
// Update stats
|
||||
var stats = this.getLogStats();
|
||||
var statBadges = document.querySelectorAll('.sh-stat-value');
|
||||
if (statBadges.length >= 3) {
|
||||
statBadges[0].textContent = stats.total;
|
||||
statBadges[1].textContent = stats.errors;
|
||||
statBadges[2].textContent = stats.warnings;
|
||||
}
|
||||
|
||||
// Update filter tabs counts
|
||||
var tabs = document.querySelectorAll('.sh-tab-label');
|
||||
if (tabs.length >= 4) {
|
||||
tabs[0].textContent = 'All Logs (' + stats.total + ')';
|
||||
tabs[1].textContent = 'Errors (' + stats.errors + ')';
|
||||
tabs[2].textContent = 'Warnings (' + stats.warnings + ')';
|
||||
tabs[3].textContent = 'Info (' + stats.info + ')';
|
||||
}
|
||||
}, this)).catch(function(err) {
|
||||
ui.hideModal();
|
||||
@ -105,6 +276,19 @@ return L.view.extend({
|
||||
});
|
||||
},
|
||||
|
||||
downloadLogs: function() {
|
||||
var filtered = this.getFilteredLogs();
|
||||
var content = filtered.join('\n');
|
||||
var blob = new Blob([content], { type: 'text/plain' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'system-logs-' + new Date().toISOString().split('T')[0] + '.txt';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
ui.addNotification(null, E('p', '✓ Logs downloaded'), 'info');
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
|
||||
@ -25,12 +25,16 @@ return view.extend({
|
||||
var theme = data[2];
|
||||
|
||||
var container = E('div', { 'class': 'system-hub-dashboard' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/overview.css') }),
|
||||
|
||||
// Header
|
||||
this.renderHeader(),
|
||||
|
||||
// Stats Overview (like SecuBox)
|
||||
this.renderStatsOverview(),
|
||||
|
||||
// Health Metrics Cards
|
||||
E('div', { 'class': 'sh-metrics-grid' }, [
|
||||
this.renderMetricCard('CPU', this.healthData.cpu),
|
||||
@ -66,24 +70,53 @@ return view.extend({
|
||||
var score = this.healthData.score || 0;
|
||||
var scoreClass = score >= 80 ? 'excellent' : (score >= 60 ? 'good' : (score >= 40 ? 'warning' : 'critical'));
|
||||
|
||||
return E('div', { 'class': 'sh-overview-header' }, [
|
||||
E('div', { 'class': 'sh-overview-title' }, [
|
||||
E('h2', {}, [
|
||||
E('span', { 'class': 'sh-title-icon' }, '🖥️'),
|
||||
' System Overview'
|
||||
return E('div', { 'class': 'sh-dashboard-header' }, [
|
||||
E('div', { 'class': 'sh-dashboard-header-content' }, [
|
||||
E('div', {}, [
|
||||
E('h2', {}, '🖥️ System Hub'),
|
||||
E('p', { 'class': 'sh-dashboard-subtitle' }, 'System Monitoring & Management Center')
|
||||
]),
|
||||
E('p', { 'class': 'sh-overview-subtitle' },
|
||||
this.sysInfo.hostname + ' • ' + this.sysInfo.model)
|
||||
]),
|
||||
E('div', { 'class': 'sh-overview-score' }, [
|
||||
E('div', { 'class': 'sh-score-circle sh-score-' + scoreClass }, [
|
||||
E('div', { 'class': 'sh-score-value' }, score),
|
||||
E('div', { 'class': 'sh-score-label' }, 'Health Score')
|
||||
E('div', { 'class': 'sh-dashboard-header-info' }, [
|
||||
E('span', { 'class': 'sh-dashboard-badge sh-dashboard-badge-version' },
|
||||
'v0.2.2'),
|
||||
E('span', { 'class': 'sh-dashboard-badge' },
|
||||
'⏱️ ' + (this.sysInfo.uptime_formatted || '0d 0h 0m')),
|
||||
E('span', { 'class': 'sh-dashboard-badge' },
|
||||
'🖥️ ' + (this.sysInfo.hostname || 'OpenWrt'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderStatsOverview: function() {
|
||||
var score = this.healthData.score || 0;
|
||||
var scoreClass = score >= 80 ? 'excellent' : (score >= 60 ? 'good' : (score >= 40 ? 'warning' : 'critical'));
|
||||
var scoreLabel = score >= 80 ? 'Excellent' : (score >= 60 ? 'Good' : (score >= 40 ? 'Warning' : 'Critical'));
|
||||
|
||||
return E('div', { 'class': 'sh-stats-overview-grid' }, [
|
||||
E('div', { 'class': 'sh-stat-overview-card sh-stat-' + scoreClass }, [
|
||||
E('div', { 'class': 'sh-stat-overview-value' }, score),
|
||||
E('div', { 'class': 'sh-stat-overview-label' }, 'Health Score'),
|
||||
E('div', { 'class': 'sh-stat-overview-status' }, scoreLabel)
|
||||
]),
|
||||
E('div', { 'class': 'sh-stat-overview-card sh-stat-cpu' }, [
|
||||
E('div', { 'class': 'sh-stat-overview-icon' }, '🔥'),
|
||||
E('div', { 'class': 'sh-stat-overview-value' }, (this.healthData.cpu?.usage || 0) + '%'),
|
||||
E('div', { 'class': 'sh-stat-overview-label' }, 'CPU Usage')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stat-overview-card sh-stat-memory' }, [
|
||||
E('div', { 'class': 'sh-stat-overview-icon' }, '💾'),
|
||||
E('div', { 'class': 'sh-stat-overview-value' }, (this.healthData.memory?.usage || 0) + '%'),
|
||||
E('div', { 'class': 'sh-stat-overview-label' }, 'Memory Usage')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stat-overview-card sh-stat-disk' }, [
|
||||
E('div', { 'class': 'sh-stat-overview-icon' }, '💿'),
|
||||
E('div', { 'class': 'sh-stat-overview-value' }, (this.healthData.disk?.usage || 0) + '%'),
|
||||
E('div', { 'class': 'sh-stat-overview-label' }, 'Disk Usage')
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderMetricCard: function(type, data) {
|
||||
if (!data) return E('div');
|
||||
|
||||
|
||||
@ -1,116 +1,292 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require dom';
|
||||
'require system-hub/api as API';
|
||||
'require system-hub/theme as Theme';
|
||||
|
||||
// Load CSS
|
||||
document.head.appendChild(E('link', {
|
||||
'rel': 'stylesheet',
|
||||
'type': 'text/css',
|
||||
'href': L.resource('system-hub/dashboard.css')
|
||||
}));
|
||||
return view.extend({
|
||||
services: [],
|
||||
currentFilter: 'all',
|
||||
searchQuery: '',
|
||||
|
||||
// Initialize theme
|
||||
Theme.init();
|
||||
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
return API.listServices();
|
||||
return Promise.all([
|
||||
API.listServices(),
|
||||
Theme.getTheme()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(services) {
|
||||
var v = E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, _('System Services')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Manage system services: start, stop, restart, enable, or disable'))
|
||||
render: function(data) {
|
||||
this.services = data[0] || [];
|
||||
var theme = data[1];
|
||||
|
||||
var container = E('div', { 'class': 'system-hub-services' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/services.css') }),
|
||||
|
||||
// Header with stats
|
||||
this.renderHeader(),
|
||||
|
||||
// Filter tabs
|
||||
this.renderFilterTabs(),
|
||||
|
||||
// Search box
|
||||
this.renderSearchBox(),
|
||||
|
||||
// Services grid
|
||||
E('div', { 'class': 'sh-services-grid', 'id': 'services-grid' })
|
||||
]);
|
||||
|
||||
var section = E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, _('Service List'))
|
||||
]);
|
||||
// Initial render
|
||||
this.updateServicesGrid();
|
||||
|
||||
if (services.length === 0) {
|
||||
section.appendChild(E('p', { 'style': 'text-align: center; font-style: italic; padding: 20px;' },
|
||||
_('No services found')));
|
||||
v.appendChild(section);
|
||||
return v;
|
||||
}
|
||||
return container;
|
||||
},
|
||||
|
||||
var table = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Service Name')),
|
||||
E('th', { 'class': 'th' }, _('Status')),
|
||||
E('th', { 'class': 'th' }, _('Autostart')),
|
||||
E('th', { 'class': 'th' }, _('Actions'))
|
||||
renderHeader: function() {
|
||||
var stats = this.getStats();
|
||||
|
||||
return E('div', { 'class': 'sh-page-header' }, [
|
||||
E('div', {}, [
|
||||
E('h2', { 'class': 'sh-page-title' }, [
|
||||
E('span', { 'class': 'sh-page-title-icon' }, '⚙️'),
|
||||
'System Services'
|
||||
]),
|
||||
E('p', { 'class': 'sh-page-subtitle' },
|
||||
'Manage system services: start, stop, restart, enable, or disable')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stats-grid' }, [
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value', 'style': 'color: #22c55e;' }, stats.running),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Running')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value', 'style': 'color: #ef4444;' }, stats.stopped),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Stopped')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value', 'style': 'color: #6366f1;' }, stats.enabled),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Enabled')
|
||||
]),
|
||||
E('div', { 'class': 'sh-stat-badge' }, [
|
||||
E('div', { 'class': 'sh-stat-value' }, stats.total),
|
||||
E('div', { 'class': 'sh-stat-label' }, 'Total')
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
services.forEach(L.bind(function(service) {
|
||||
var statusColor = service.running ? 'green' : 'red';
|
||||
var statusText = service.running ? '● Running' : '○ Stopped';
|
||||
var enabledText = service.enabled ? '✓ Enabled' : '✗ Disabled';
|
||||
renderFilterTabs: function() {
|
||||
var self = this;
|
||||
var stats = this.getStats();
|
||||
|
||||
var actionsDiv = E('div', { 'style': 'display: flex; gap: 5px;' });
|
||||
return E('div', { 'class': 'sh-filter-tabs' }, [
|
||||
this.createFilterTab('all', '📋', 'All Services', stats.total),
|
||||
this.createFilterTab('running', '▶️', 'Running', stats.running),
|
||||
this.createFilterTab('stopped', '⏹️', 'Stopped', stats.stopped),
|
||||
this.createFilterTab('enabled', '✓', 'Enabled', stats.enabled),
|
||||
this.createFilterTab('disabled', '✗', 'Disabled', stats.disabled)
|
||||
]);
|
||||
},
|
||||
|
||||
// Start button
|
||||
if (!service.running) {
|
||||
actionsDiv.appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-positive',
|
||||
'click': L.bind(function(service_name, ev) {
|
||||
this.performAction(service_name, 'start');
|
||||
}, this, service.name)
|
||||
}, _('Start')));
|
||||
createFilterTab: function(filter, icon, label, count) {
|
||||
var self = this;
|
||||
var isActive = this.currentFilter === filter;
|
||||
|
||||
return E('div', {
|
||||
'class': 'sh-filter-tab' + (isActive ? ' active' : ''),
|
||||
'click': function() {
|
||||
self.currentFilter = filter;
|
||||
self.updateFilterTabs();
|
||||
self.updateServicesGrid();
|
||||
}
|
||||
}, [
|
||||
E('span', { 'class': 'sh-tab-icon' }, icon),
|
||||
E('span', { 'class': 'sh-tab-label' }, label + ' (' + count + ')')
|
||||
]);
|
||||
},
|
||||
|
||||
// Stop button
|
||||
if (service.running) {
|
||||
actionsDiv.appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-negative',
|
||||
'click': L.bind(function(service_name, ev) {
|
||||
this.performAction(service_name, 'stop');
|
||||
}, this, service.name)
|
||||
}, _('Stop')));
|
||||
}
|
||||
renderSearchBox: function() {
|
||||
var self = this;
|
||||
|
||||
// Restart button
|
||||
actionsDiv.appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': L.bind(function(service_name, ev) {
|
||||
this.performAction(service_name, 'restart');
|
||||
}, this, service.name)
|
||||
}, _('Restart')));
|
||||
return E('div', { 'style': 'margin-bottom: 24px;' }, [
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'placeholder': '🔍 Search services...',
|
||||
'style': 'width: 100%; padding: 12px 16px; border-radius: 8px; border: 1px solid var(--sh-border); background: var(--sh-bg-card); color: var(--sh-text-primary); font-size: 14px;',
|
||||
'input': function(ev) {
|
||||
self.searchQuery = ev.target.value.toLowerCase();
|
||||
self.updateServicesGrid();
|
||||
}
|
||||
})
|
||||
]);
|
||||
},
|
||||
|
||||
// Enable/Disable button
|
||||
if (service.enabled) {
|
||||
actionsDiv.appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
'click': L.bind(function(service_name, ev) {
|
||||
this.performAction(service_name, 'disable');
|
||||
}, this, service.name)
|
||||
}, _('Disable')));
|
||||
updateFilterTabs: function() {
|
||||
var tabs = document.querySelectorAll('.sh-filter-tab');
|
||||
tabs.forEach(function(tab, index) {
|
||||
var filters = ['all', 'running', 'stopped', 'enabled', 'disabled'];
|
||||
if (filters[index] === this.currentFilter) {
|
||||
tab.classList.add('active');
|
||||
} else {
|
||||
actionsDiv.appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': L.bind(function(service_name, ev) {
|
||||
this.performAction(service_name, 'enable');
|
||||
}, this, service.name)
|
||||
}, _('Enable')));
|
||||
tab.classList.remove('active');
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
updateServicesGrid: function() {
|
||||
var grid = document.getElementById('services-grid');
|
||||
if (!grid) return;
|
||||
|
||||
var filtered = this.getFilteredServices();
|
||||
|
||||
if (filtered.length === 0) {
|
||||
dom.content(grid, [
|
||||
E('div', { 'class': 'sh-empty-state' }, [
|
||||
E('div', { 'class': 'sh-empty-icon' }, '📭'),
|
||||
E('div', { 'class': 'sh-empty-text' },
|
||||
this.searchQuery ? 'No services match your search' : 'No services found')
|
||||
])
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
dom.content(grid, filtered.map(this.renderServiceCard, this));
|
||||
},
|
||||
|
||||
getFilteredServices: function() {
|
||||
return this.services.filter(function(service) {
|
||||
// Apply filter
|
||||
var matchesFilter = true;
|
||||
switch (this.currentFilter) {
|
||||
case 'running':
|
||||
matchesFilter = service.running;
|
||||
break;
|
||||
case 'stopped':
|
||||
matchesFilter = !service.running;
|
||||
break;
|
||||
case 'enabled':
|
||||
matchesFilter = service.enabled;
|
||||
break;
|
||||
case 'disabled':
|
||||
matchesFilter = !service.enabled;
|
||||
break;
|
||||
}
|
||||
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td' }, E('strong', {}, service.name)),
|
||||
E('td', { 'class': 'td' }, [
|
||||
E('span', { 'style': 'color: ' + statusColor + '; font-weight: bold;' }, statusText)
|
||||
// Apply search
|
||||
var matchesSearch = !this.searchQuery ||
|
||||
service.name.toLowerCase().includes(this.searchQuery);
|
||||
|
||||
return matchesFilter && matchesSearch;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
renderServiceCard: function(service) {
|
||||
var statusClass = service.running ? 'ok' : 'error';
|
||||
var statusIcon = service.running ? '▶️' : '⏹️';
|
||||
var statusText = service.running ? 'Running' : 'Stopped';
|
||||
var enabledIcon = service.enabled ? '✓' : '✗';
|
||||
var enabledText = service.enabled ? 'Enabled' : 'Disabled';
|
||||
|
||||
return E('div', { 'class': 'sh-card' }, [
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
E('h3', { 'class': 'sh-card-title' }, [
|
||||
E('span', { 'class': 'sh-card-title-icon' }, '⚙️'),
|
||||
service.name
|
||||
]),
|
||||
E('td', { 'class': 'td' }, enabledText),
|
||||
E('td', { 'class': 'td' }, actionsDiv)
|
||||
E('div', { 'class': 'sh-card-badge sh-status-badge sh-status-' + statusClass }, [
|
||||
statusIcon + ' ' + statusText
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-body' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 8px; margin-bottom: 16px;' }, [
|
||||
E('span', { 'style': 'font-size: 16px;' }, enabledIcon),
|
||||
E('span', { 'style': 'font-weight: 600; color: var(--sh-text-secondary);' },
|
||||
'Autostart: ' + enabledText)
|
||||
]),
|
||||
E('div', { 'style': 'display: flex; gap: 8px; flex-wrap: wrap;' },
|
||||
this.renderActionButtons(service)
|
||||
)
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderActionButtons: function(service) {
|
||||
var buttons = [];
|
||||
|
||||
// Start button (only if stopped)
|
||||
if (!service.running) {
|
||||
buttons.push(E('button', {
|
||||
'class': 'sh-btn sh-btn-success',
|
||||
'click': L.bind(this.performAction, this, service.name, 'start')
|
||||
}, [
|
||||
E('span', {}, '▶️'),
|
||||
E('span', {}, 'Start')
|
||||
]));
|
||||
}, this));
|
||||
}
|
||||
|
||||
section.appendChild(table);
|
||||
v.appendChild(section);
|
||||
// Stop button (only if running)
|
||||
if (service.running) {
|
||||
buttons.push(E('button', {
|
||||
'class': 'sh-btn sh-btn-danger',
|
||||
'click': L.bind(this.performAction, this, service.name, 'stop')
|
||||
}, [
|
||||
E('span', {}, '⏹️'),
|
||||
E('span', {}, 'Stop')
|
||||
]));
|
||||
}
|
||||
|
||||
return v;
|
||||
// Restart button
|
||||
buttons.push(E('button', {
|
||||
'class': 'sh-btn sh-btn-warning',
|
||||
'click': L.bind(this.performAction, this, service.name, 'restart')
|
||||
}, [
|
||||
E('span', {}, '🔄'),
|
||||
E('span', {}, 'Restart')
|
||||
]));
|
||||
|
||||
// Enable/Disable button
|
||||
if (service.enabled) {
|
||||
buttons.push(E('button', {
|
||||
'class': 'sh-btn sh-btn-secondary',
|
||||
'click': L.bind(this.performAction, this, service.name, 'disable')
|
||||
}, [
|
||||
E('span', {}, '✗'),
|
||||
E('span', {}, 'Disable')
|
||||
]));
|
||||
} else {
|
||||
buttons.push(E('button', {
|
||||
'class': 'sh-btn sh-btn-primary',
|
||||
'click': L.bind(this.performAction, this, service.name, 'enable')
|
||||
}, [
|
||||
E('span', {}, '✓'),
|
||||
E('span', {}, 'Enable')
|
||||
]));
|
||||
}
|
||||
|
||||
return buttons;
|
||||
},
|
||||
|
||||
getStats: function() {
|
||||
var stats = {
|
||||
total: this.services.length,
|
||||
running: 0,
|
||||
stopped: 0,
|
||||
enabled: 0,
|
||||
disabled: 0
|
||||
};
|
||||
|
||||
this.services.forEach(function(service) {
|
||||
if (service.running) stats.running++;
|
||||
else stats.stopped++;
|
||||
if (service.enabled) stats.enabled++;
|
||||
else stats.disabled++;
|
||||
});
|
||||
|
||||
return stats;
|
||||
},
|
||||
|
||||
performAction: function(service, action) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user