From b6d9f3a549afbe2ad4c2a577f893686c7237475f Mon Sep 17 00:00:00 2001 From: perco Date: Fri, 6 Mar 2026 22:27:52 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20PercoHub=20v1=20=E2=80=94=20dashboard?= =?UTF-8?q?=20centralis=C3=A9=20homelab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 95 +++++++++++++ api.php | 184 +++++++++++++++++++++++++ assets/app.js | 258 +++++++++++++++++++++++++++++++++++ assets/style.css | 157 +++++++++++++++++++++ index.php | 85 ++++++++++++ services.json | 349 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1128 insertions(+) create mode 100644 README.md create mode 100644 api.php create mode 100644 assets/app.js create mode 100644 assets/style.css create mode 100644 index.php create mode 100644 services.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..682e867 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# PercoHub ๐Ÿ  + +Dashboard centralisรฉ pour accรฉder ร  tous les services et projets du homelab de Perco. + +## Stack + +- **Backend** : PHP 8 (lecture `services.json`, check Docker + HTTP) +- **Frontend** : Tailwind CSS CDN + Vanilla JS +- **Donnรฉes** : `services.json` โ€” catalogue รฉditable manuellement + +## Dรฉmarrage + +```bash +nohup php -S 0.0.0.0:9300 -t /home/perco/projects/percohub/ > /tmp/percohub-php.log 2>&1 & +``` + +**URL locale** : http://192.168.1.29:9300 + +## Fonctionnalitรฉs + +- โœ… Vue centralisรฉe de tous les services (Docker + projets PHP) +- โœ… Status live (Docker inspect + HTTP ping) +- โœ… Catรฉgories : Projets Luca / Infrastructure / Mรฉdia / Domotique / Productivitรฉ / VPN Firefox +- โœ… Filtre par catรฉgorie (navbar) +- โœ… Stats globales (online/offline) +- โœ… Auto-refresh toutes les 30s +- โœ… Clic sur une carte โ†’ ouvre l'URL dans un nouvel onglet +- โœ… Dark theme cohรฉrent avec les autres projets + +## Structure + +``` +percohub/ +โ”œโ”€โ”€ index.php # Page principale (shell HTML) +โ”œโ”€โ”€ api.php # Backend : statuts Docker + HTTP +โ”œโ”€โ”€ services.json # Catalogue des services (ร  รฉditer) +โ”œโ”€โ”€ assets/ +โ”‚ โ”œโ”€โ”€ style.css # Dark theme custom +โ”‚ โ””โ”€โ”€ app.js # Frontend : rendu, refresh, filtres +โ””โ”€โ”€ README.md +``` + +## Ajouter un service + +ร‰diter `services.json` โ€” ajouter un objet dans le tableau `services` de la bonne catรฉgorie : + +```json +{ + "name": "Mon Service", + "desc": "Description courte", + "url": "http://192.168.1.29:PORT", + "docker": "nom-du-container", + "tags": ["tag1", "tag2"] +} +``` + +- `url` : null si pas d'interface web +- `docker` : null si ce n'est pas un container Docker (ex: serveur PHP) +- Si `docker` est renseignรฉ โ†’ status via `docker inspect` +- Si seulement `url` โ†’ status via HTTP ping + +## Ajouter une catรฉgorie + +Dans `services.json`, ajouter dans le tableau `categories` : + +```json +{ + "id": "ma-categorie", + "name": "Ma Catรฉgorie", + "icon": "๐Ÿ”Œ", + "color": "violet", + "services": [...] +} +``` + +**Couleurs disponibles** : violet, slate, amber, emerald, sky, orange, rose + +## API + +| Endpoint | Description | +|----------|-------------| +| `/api.php?action=all` | Tous les services avec statuts | +| `/api.php?action=config` | Catalogue brut (sans checks) | +| `/api.php?action=docker` | Liste tous les containers Docker | + +## Catรฉgories actuelles + +| Catรฉgorie | Services | +|-----------|----------| +| ๐Ÿ› ๏ธ Projets Luca | AgentStats, CronViz, MonStatut, AgentChat, HomeStatus | +| ๐Ÿ”ง Infrastructure | Portainer, Traefik, Grafana, InfluxDB, Dashdot, Watchtower | +| ๐ŸŽฌ Mรฉdia | Plex, Jellyseerr, Sonarr, Radarr, Lidarr, Prowlarr, Jackett, qBittorrent, Pinchflat, Tautulli | +| ๐Ÿ  Domotique | Home Assistant, ESPhome, Syncthing, Grott, Bresser Live, Mosquitto | +| ๐Ÿ’ผ Productivitรฉ | Nextcloud, Gitea, VSCode, Planka, Actual Budget, CronMaster, Homarr | +| ๐ŸฆŠ VPN Firefox | retak, coucouze, roxxor, xouz, nale | diff --git a/api.php b/api.php new file mode 100644 index 0000000..88c9fcc --- /dev/null +++ b/api.php @@ -0,0 +1,184 @@ +/dev/null"); + if (empty($raw)) return ['status' => 'absent', 'health' => null]; + + $parts = explode('|', trim($raw)); + $status = $parts[0] ?? 'unknown'; + $health = ($parts[1] ?? '') ?: null; + if ($health === '') $health = null; + + return ['status' => $status, 'health' => $health]; +} + +function httpPing(string $url, int $timeout = 3): array { + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_NOBODY => true, + CURLOPT_TIMEOUT => $timeout, + CURLOPT_CONNECTTIMEOUT => $timeout, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_USERAGENT => 'PercoHub/1.0', + ]); + curl_exec($ch); + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $time = (int) (curl_getinfo($ch, CURLINFO_TOTAL_TIME) * 1000); + curl_close($ch); + + $ok = $code >= 200 && $code < 400; + return ['http_code' => $code, 'ok' => $ok, 'ms' => $time]; +} + +function resolveServiceStatus(array $service): array { + $dockerName = $service['docker'] ?? null; + $url = $service['url'] ?? null; + + // Self-check: ce service est le dashboard lui-mรชme โ†’ toujours online + if (!empty($service['self'])) { + return ['online' => true, 'status_text' => 'online', 'docker' => null, 'http' => null]; + } + + $result = [ + 'online' => false, + 'status_text' => 'inconnu', + 'docker' => null, + 'http' => null, + ]; + + // 1. Docker check + if ($dockerName) { + $d = dockerStatus($dockerName); + $result['docker'] = $d; + + if ($d['status'] === 'running') { + $result['online'] = true; + $result['status_text'] = $d['health'] === 'healthy' ? 'healthy' : 'running'; + } elseif ($d['status'] === 'absent') { + $result['status_text'] = 'absent'; + } else { + $result['status_text'] = $d['status']; // exited, paused, restarting... + } + } + + // 2. HTTP check (for PHP projects without docker, or in addition) + if ($url && !$dockerName) { + $h = httpPing($url); + $result['http'] = $h; + $result['online'] = $h['ok']; + $result['status_text'] = $h['ok'] ? 'online' : ($h['http_code'] > 0 ? "http {$h['http_code']}" : 'offline'); + } + + return $result; +} + +// โ”€โ”€โ”€ Routes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +$configFile = __DIR__ . '/services.json'; +if (!file_exists($configFile)) { + echo json_encode(['error' => 'services.json introuvable'], JSON_UNESCAPED_UNICODE); + exit; +} + +$config = json_decode(file_get_contents($configFile), true); +if (!$config) { + echo json_encode(['error' => 'services.json invalide'], JSON_UNESCAPED_UNICODE); + exit; +} + +if ($action === 'status') { + // Statut d'un seul service + $catId = $_GET['cat'] ?? null; + $svcIdx = (int)($_GET['idx'] ?? -1); + + $found = null; + foreach ($config['categories'] as $cat) { + if ($cat['id'] === $catId && isset($cat['services'][$svcIdx])) { + $found = $cat['services'][$svcIdx]; + break; + } + } + + if (!$found) { + echo json_encode(['error' => 'service introuvable'], JSON_UNESCAPED_UNICODE); + exit; + } + + echo json_encode(resolveServiceStatus($found), JSON_UNESCAPED_UNICODE); + exit; +} + +if ($action === 'all') { + // Tous les services avec leur statut + $output = ['categories' => [], 'generated_at' => date('c'), 'total' => 0, 'online' => 0]; + + foreach ($config['categories'] as $cat) { + $catOut = [ + 'id' => $cat['id'], + 'name' => $cat['name'], + 'icon' => $cat['icon'], + 'color' => $cat['color'], + 'services' => [], + ]; + + foreach ($cat['services'] as $svc) { + $statusInfo = resolveServiceStatus($svc); + + $catOut['services'][] = array_merge($svc, [ + 'status' => $statusInfo, + ]); + + $output['total']++; + if ($statusInfo['online']) $output['online']++; + } + + $output['categories'][] = $catOut; + } + + $output['offline'] = $output['total'] - $output['online']; + + echo json_encode($output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; +} + +if ($action === 'config') { + // Catalogue brut sans status checks + echo json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; +} + +if ($action === 'docker') { + // Liste tous les containers Docker + $raw = shell_exec("docker ps -a --format '{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}' 2>/dev/null"); + $containers = []; + foreach (explode("\n", trim($raw)) as $line) { + if (empty($line)) continue; + $parts = explode('|', $line); + $containers[] = [ + 'name' => $parts[0] ?? '', + 'image' => $parts[1] ?? '', + 'status' => $parts[2] ?? '', + 'ports' => $parts[3] ?? '', + ]; + } + echo json_encode(['containers' => $containers, 'count' => count($containers)], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; +} + +echo json_encode(['error' => 'action invalide. Utilisez: all, status, config, docker'], JSON_UNESCAPED_UNICODE); diff --git a/assets/app.js b/assets/app.js new file mode 100644 index 0000000..4c824ee --- /dev/null +++ b/assets/app.js @@ -0,0 +1,258 @@ +/** + * PercoHub โ€” Frontend JS + * Charge les services depuis api.php, rend les catรฉgories/cards, auto-refresh. + */ + +// โ”€โ”€ Config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +const REFRESH_INTERVAL = 30; // secondes +const API_BASE = '/api.php'; + +// โ”€โ”€ State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +let refreshTimer = null; +let countdown = REFRESH_INTERVAL; +let activeFilter = 'all'; +let lastData = null; + +// โ”€โ”€ Init โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +document.addEventListener('DOMContentLoaded', () => { + loadAll(); + startCountdown(); +}); + +// โ”€โ”€ Load โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +async function loadAll(forceShow = false) { + if (forceShow) { + showLoader(); + } + + try { + const res = await fetch(`${API_BASE}?action=all`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + lastData = data; + + renderDashboard(data); + renderGlobalStats(data); + renderFilterBar(data); + hideLoader(); + resetCountdown(); + + } catch (err) { + showError(err.message); + } +} + +// โ”€โ”€ Render โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function renderDashboard(data) { + const dash = document.getElementById('dashboard'); + dash.innerHTML = ''; + + data.categories.forEach(cat => { + if (activeFilter !== 'all' && cat.id !== activeFilter) return; + + const section = document.createElement('section'); + section.dataset.cat = cat.id; + + // Header + const onlineCount = cat.services.filter(s => s.status?.online).length; + section.innerHTML = ` +
+ ${cat.icon} + ${cat.name} + ${cat.services.length} services + ${onlineCount}/${cat.services.length} en ligne +
+
+ `; + + dash.appendChild(section); + + const grid = section.querySelector(`#grid-${cat.id}`); + cat.services.forEach(svc => { + grid.appendChild(buildCard(svc, cat.color)); + }); + }); + + dash.classList.remove('hidden'); +} + +function buildCard(svc, catColor) { + const status = svc.status || {}; + const isOnline = status.online; + const statusText = status.status_text || 'inconnu'; + const hasUrl = !!svc.url; + const hasNote = !!svc.note; + + // Status dot class + let dotClass = 'loading'; + let badgeClass = 'badge-gray'; + let badgeText = statusText; + + if (statusText === 'running' || statusText === 'online') { + dotClass = 'online'; badgeClass = 'badge-green'; badgeText = 'running'; + } else if (statusText === 'healthy') { + dotClass = 'online'; badgeClass = 'badge-green'; badgeText = 'healthy'; + } else if (statusText === 'offline' || statusText === 'absent' || statusText === 'exited') { + dotClass = 'offline'; badgeClass = 'badge-red'; + } else if (statusText === 'restarting') { + dotClass = 'warn'; badgeClass = 'badge-yellow'; + } else if (statusText === 'inconnu') { + dotClass = 'loading'; badgeClass = 'badge-gray'; + } + + // Response time badge + let timeBadge = ''; + if (status.http?.ms && status.http.ms > 0) { + timeBadge = `${status.http.ms}ms`; + } + + // Docker badge + let dockerBadge = ''; + if (svc.docker) { + dockerBadge = `๐Ÿณ`; + } + + // Note + let noteBadge = hasNote ? `${svc.note}` : ''; + + // Tags + const tagsHtml = (svc.tags || []).map(t => + `#${t}` + ).join(''); + + const card = document.createElement('div'); + card.className = `service-card cat-${catColor} ${hasUrl ? 'has-link' : ''}`; + + if (hasUrl) { + card.setAttribute('onclick', `openService('${svc.url}', '${svc.name}')`); + card.setAttribute('title', `Ouvrir ${svc.name}`); + } + + card.innerHTML = ` +
+
+ + ${svc.name} +
+
+ ${dockerBadge} + ${badgeText} + ${timeBadge} +
+
+

${svc.desc}

+
+
${tagsHtml}
+ ${noteBadge} + ${hasUrl ? `${formatUrl(svc.url)}` : ''} +
+ `; + + return card; +} + +// โ”€โ”€ Global stats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function renderGlobalStats(data) { + const el = document.getElementById('global-stats'); + const total = data.total || 0; + const online = data.online || 0; + const offline = total - online; + + el.innerHTML = ` + + ${online} + + + ${offline} + + + ${total} services + + `; +} + +// โ”€โ”€ Filter bar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function renderFilterBar(data) { + const bar = document.getElementById('filter-bar'); + bar.innerHTML = ` + + `; + + data.categories.forEach(cat => { + const btn = document.createElement('button'); + btn.className = `filter-btn ${activeFilter === cat.id ? 'active' : ''}`; + btn.textContent = `${cat.icon} ${cat.name}`; + btn.onclick = () => setFilter(cat.id); + bar.appendChild(btn); + }); +} + +function setFilter(id) { + activeFilter = id; + if (lastData) { + renderDashboard(lastData); + renderFilterBar(lastData); + } +} + +// โ”€โ”€ Countdown & refresh โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function startCountdown() { + refreshTimer = setInterval(() => { + countdown--; + const el = document.getElementById('refresh-timer'); + if (el) el.textContent = `โ†ป ${countdown}s`; + + if (countdown <= 0) { + loadAll(); + } + }, 1000); +} + +function resetCountdown() { + countdown = REFRESH_INTERVAL; +} + +// โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function openService(url, name) { + window.open(url, '_blank', 'noopener'); +} + +function formatUrl(url) { + try { + const u = new URL(url); + return u.host + (u.port ? '' : '') + (u.pathname !== '/' ? u.pathname : ''); + } catch { + return url; + } +} + +function showLoader() { + document.getElementById('loader').classList.remove('hidden'); + document.getElementById('dashboard').classList.add('hidden'); + document.getElementById('error-panel').classList.add('hidden'); +} + +function hideLoader() { + document.getElementById('loader').classList.add('hidden'); +} + +function showError(msg) { + document.getElementById('loader').classList.add('hidden'); + document.getElementById('error-panel').classList.remove('hidden'); + document.getElementById('error-msg').textContent = msg; +} + +function closeModal(e) { + if (e.target === document.getElementById('modal')) { + document.getElementById('modal').classList.add('hidden'); + } +} diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..a64a23f --- /dev/null +++ b/assets/style.css @@ -0,0 +1,157 @@ +/* PercoHub โ€” Custom styles (complรฉment Tailwind) */ + +/* โ”€โ”€ Scrollbar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: #0d1117; } +::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #484f58; } + +/* โ”€โ”€ Service card โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.service-card { + background: #161b22; + border: 1px solid #30363d; + border-radius: 0.75rem; + padding: 1rem 1.25rem; + transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; + position: relative; + overflow: hidden; + cursor: default; +} +.service-card.has-link { + cursor: pointer; +} +.service-card.has-link:hover { + border-color: #484f58; + box-shadow: 0 0 0 1px #484f58, 0 4px 16px rgba(0,0,0,0.4); + transform: translateY(-1px); +} + +/* Left accent bar */ +.service-card::before { + content: ''; + position: absolute; + left: 0; top: 0; bottom: 0; + width: 3px; + border-radius: 3px 0 0 3px; + background: var(--accent-color, #30363d); +} + +/* โ”€โ”€ Category accent colors โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.cat-violet { --accent-color: #8b5cf6; } +.cat-slate { --accent-color: #64748b; } +.cat-amber { --accent-color: #f59e0b; } +.cat-emerald { --accent-color: #10b981; } +.cat-sky { --accent-color: #0ea5e9; } +.cat-orange { --accent-color: #f97316; } +.cat-rose { --accent-color: #f43f5e; } + +/* โ”€โ”€ Status dot โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + flex-shrink: 0; +} +.status-dot.online { background: #10b981; box-shadow: 0 0 6px #10b981; } +.status-dot.offline { background: #ef4444; } +.status-dot.warn { background: #f59e0b; box-shadow: 0 0 6px #f59e0b; } +.status-dot.loading { background: #4b5563; animation: pulse-dot 1.2s infinite; } + +@keyframes pulse-dot { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 1; } +} + +/* โ”€โ”€ Status badge โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.badge { + font-size: 0.65rem; + padding: 0.15rem 0.5rem; + border-radius: 9999px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; +} +.badge-green { background: #10b98120; color: #34d399; border: 1px solid #10b98140; } +.badge-red { background: #ef444420; color: #f87171; border: 1px solid #ef444440; } +.badge-yellow { background: #f59e0b20; color: #fbbf24; border: 1px solid #f59e0b40; } +.badge-gray { background: #8b949e20; color: #8b949e; border: 1px solid #8b949e40; } +.badge-blue { background: #0ea5e920; color: #38bdf8; border: 1px solid #0ea5e940; } + +/* โ”€โ”€ Section header โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.section-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid #30363d; +} +.section-title { + font-size: 1rem; + font-weight: 700; + color: #e6edf3; +} +.section-count { + font-size: 0.75rem; + color: #8b949e; + background: #21262d; + border: 1px solid #30363d; + border-radius: 9999px; + padding: 0.1rem 0.55rem; +} + +/* โ”€โ”€ Global stats pills โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.stat-pill { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.75rem; + padding: 0.2rem 0.65rem; + border-radius: 9999px; + border: 1px solid; + font-weight: 500; +} + +/* โ”€โ”€ Filter button โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.filter-btn { + font-size: 0.7rem; + padding: 0.2rem 0.6rem; + border-radius: 9999px; + border: 1px solid #30363d; + background: transparent; + color: #8b949e; + cursor: pointer; + transition: all 0.15s; +} +.filter-btn:hover, +.filter-btn.active { + border-color: #8b5cf6; + color: #c4b5fd; + background: #8b5cf620; +} + +/* โ”€โ”€ Note badge โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.note-tag { + font-size: 0.6rem; + padding: 0.1rem 0.4rem; + border-radius: 4px; + background: #21262d; + color: #8b949e; + border: 1px solid #30363d; + max-width: 160px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* โ”€โ”€ Responsive grid โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.services-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 0.85rem; +} + +@media (max-width: 640px) { + .services-grid { grid-template-columns: 1fr; } +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..efe3e60 --- /dev/null +++ b/index.php @@ -0,0 +1,85 @@ + + + + + + PercoHub โ€” Dashboard + + + + + + + + + + +
+ + +
+
+

Chargement des servicesโ€ฆ

+
+ + + + + + + +
+ + + + + + + diff --git a/services.json b/services.json new file mode 100644 index 0000000..4268a84 --- /dev/null +++ b/services.json @@ -0,0 +1,349 @@ +{ + "categories": [ + { + "id": "luca", + "name": "Projets Luca", + "icon": "๐Ÿ› ๏ธ", + "color": "violet", + "services": [ + { + "name": "PercoHub", + "desc": "Ce dashboard centralisรฉ", + "url": "http://192.168.1.29:9300", + "docker": null, + "self": true, + "tags": ["dashboard"] + }, + { + "name": "AgentStats", + "desc": "Stats & coรปts des agents IA", + "url": "http://192.168.1.29:9000", + "docker": null, + "tags": ["ia", "monitoring"] + }, + { + "name": "CronViz", + "desc": "Visualisation des cron jobs OpenClaw", + "url": "http://192.168.1.29:9100", + "docker": null, + "tags": ["cron", "monitoring"] + }, + { + "name": "MonStatut", + "desc": "Monitoring uptime services rรฉseau", + "url": "http://192.168.1.29:9200", + "docker": null, + "tags": ["monitoring"] + }, + { + "name": "AgentChat", + "desc": "Interface chat avec les agents IA", + "url": null, + "docker": null, + "tags": ["ia", "chat"], + "note": "Non dรฉmarrรฉ โ€” port ร  dรฉfinir" + }, + { + "name": "HomeStatus", + "desc": "Stats systรจme temps rรฉel (CPU, RAM, disques)", + "url": null, + "docker": null, + "tags": ["monitoring", "system"], + "note": "Non dรฉmarrรฉ โ€” port ร  dรฉfinir" + } + ] + }, + { + "id": "infra", + "name": "Infrastructure", + "icon": "๐Ÿ”ง", + "color": "slate", + "services": [ + { + "name": "Portainer", + "desc": "Gestion visuelle des conteneurs Docker", + "url": "https://portainer.nas.percolouco.com", + "docker": "portainer", + "tags": ["docker", "admin"] + }, + { + "name": "Traefik", + "desc": "Reverse proxy โ€” routing & certificats SSL", + "url": "http://192.168.1.29:8004", + "docker": "traefik", + "tags": ["proxy", "ssl"] + }, + { + "name": "Grafana", + "desc": "Dashboards mรฉtriques & visualisation", + "url": "http://192.168.1.29:3010", + "docker": "grafana", + "tags": ["metrics", "monitoring"] + }, + { + "name": "InfluxDB", + "desc": "Base de donnรฉes time-series", + "url": "http://192.168.1.29:8086", + "docker": "influxdb", + "tags": ["database", "metrics"] + }, + { + "name": "Dashdot", + "desc": "Dashboard systรจme moderne (CPU, RAM, rรฉseau)", + "url": "http://192.168.1.29:3001", + "docker": "dashboard-dash-1", + "tags": ["system", "monitoring"] + }, + { + "name": "Watchtower", + "desc": "Mises ร  jour automatiques des conteneurs", + "url": null, + "docker": "watchtower", + "tags": ["docker", "updates"] + }, + { + "name": "Gitea Runner", + "desc": "Runner CI/CD pour Gitea Actions", + "url": null, + "docker": "gitea-runner", + "tags": ["ci-cd"] + } + ] + }, + { + "id": "media", + "name": "Mรฉdia", + "icon": "๐ŸŽฌ", + "color": "amber", + "services": [ + { + "name": "Plex", + "desc": "Serveur multimรฉdia โ€” films, sรฉries, musique", + "url": "http://192.168.1.29:32400/web", + "docker": "plex", + "tags": ["streaming", "media"] + }, + { + "name": "Jellyseerr", + "desc": "Demandes de contenu mรฉdia", + "url": "http://192.168.1.29:5055", + "docker": "jellyseerr", + "tags": ["media", "requests"] + }, + { + "name": "Sonarr", + "desc": "Gestion automatique des sรฉries TV", + "url": "http://192.168.1.29:8989", + "docker": "sonarr", + "tags": ["tv", "automation"] + }, + { + "name": "Radarr", + "desc": "Gestion automatique des films", + "url": "http://192.168.1.29:7878", + "docker": "radarr", + "tags": ["movies", "automation"] + }, + { + "name": "Lidarr", + "desc": "Gestion automatique de la musique", + "url": "http://192.168.1.29:8686", + "docker": "lidarr", + "tags": ["music", "automation"] + }, + { + "name": "Prowlarr", + "desc": "Gestionnaire d'indexeurs", + "url": "http://192.168.1.29:9696", + "docker": "prowlarr", + "tags": ["indexer"] + }, + { + "name": "Jackett", + "desc": "Proxy indexeurs torrents", + "url": "http://192.168.1.29:9117", + "docker": "jackett", + "tags": ["indexer", "torrent"] + }, + { + "name": "qBittorrent", + "desc": "Client torrent (via VPN Gluetun)", + "url": "http://192.168.1.29:1234", + "docker": "qbittorrent", + "tags": ["torrent", "vpn"] + }, + { + "name": "Pinchflat", + "desc": "Tรฉlรฉchargement automatique YouTube", + "url": "http://192.168.1.29:8945", + "docker": "pinchflat-pinchflat-1", + "tags": ["youtube", "download"] + }, + { + "name": "Tautulli", + "desc": "Statistiques et monitoring Plex", + "url": "http://192.168.1.29:8181", + "docker": "tautulli", + "tags": ["plex", "stats"] + }, + { + "name": "FlareSolverr", + "desc": "Bypass Cloudflare pour indexeurs", + "url": "http://192.168.1.29:8191", + "docker": "flaresolverr", + "tags": ["proxy"] + } + ] + }, + { + "id": "domotique", + "name": "Domotique", + "icon": "๐Ÿ ", + "color": "emerald", + "services": [ + { + "name": "Home Assistant", + "desc": "Plateforme domotique centrale", + "url": "http://192.168.1.29:8123", + "docker": "homeassistant", + "tags": ["domotique", "automation"] + }, + { + "name": "ESPhome", + "desc": "Firmware & gestion des devices ESP", + "url": null, + "docker": "esphome", + "tags": ["esp", "firmware"] + }, + { + "name": "Syncthing", + "desc": "Synchronisation de fichiers P2P", + "url": "http://192.168.1.29:8384", + "docker": "syncthing", + "tags": ["sync", "files"] + }, + { + "name": "Grott", + "desc": "Monitoring panneaux solaires Growatt", + "url": "http://192.168.1.29:5279", + "docker": "grott", + "tags": ["solar", "energy"] + }, + { + "name": "Bresser Live", + "desc": "Station mรฉtรฉo Bresser en temps rรฉel", + "url": "http://192.168.1.29:8887", + "docker": "bresser-live", + "tags": ["weather", "sensors"] + }, + { + "name": "Mosquitto", + "desc": "Broker MQTT", + "url": null, + "docker": "mosquitto", + "tags": ["mqtt", "iot"] + } + ] + }, + { + "id": "productivite", + "name": "Productivitรฉ", + "icon": "๐Ÿ’ผ", + "color": "sky", + "services": [ + { + "name": "Nextcloud", + "desc": "Cloud personnel โ€” fichiers, calendrier, contacts", + "url": "http://192.168.1.29:8080", + "docker": "nextcloud", + "tags": ["cloud", "files"] + }, + { + "name": "Gitea", + "desc": "Hรฉbergement Git self-hosted", + "url": "http://192.168.1.29:3500", + "docker": "gitea", + "tags": ["git", "dev"] + }, + { + "name": "VSCode", + "desc": "ร‰diteur de code dans le navigateur", + "url": "http://192.168.1.29:8888", + "docker": "vscode", + "tags": ["dev", "editor"] + }, + { + "name": "Planka", + "desc": "Gestion de projets Kanban", + "url": "https://planka.nas.percolouco.com", + "docker": "planka", + "tags": ["kanban", "projects"] + }, + { + "name": "Actual Budget", + "desc": "Gestion budget personnel", + "url": "http://192.168.1.29:5006", + "docker": "actual-server", + "tags": ["finance", "budget"] + }, + { + "name": "CronMaster", + "desc": "Gestionnaire de tรขches cron", + "url": "http://192.168.1.29:40123", + "docker": "cronmaster", + "tags": ["cron", "scheduler"] + }, + { + "name": "Homarr", + "desc": "Dashboard de dรฉmarrage alternatif", + "url": "http://192.168.1.29:7575", + "docker": "homarr", + "tags": ["dashboard"] + } + ] + }, + { + "id": "vpn", + "name": "VPN Firefox", + "icon": "๐ŸฆŠ", + "color": "orange", + "services": [ + { + "name": "Firefox โ€” retak", + "desc": "Instance Firefox via VPN Gluetun", + "url": "http://192.168.1.29:3000", + "docker": "vpn_mh_retak", + "tags": ["vpn", "browser"] + }, + { + "name": "Firefox โ€” coucouze", + "desc": "Instance Firefox via VPN Gluetun", + "url": "http://192.168.1.29:3100", + "docker": "vpn_mh_coucouze", + "tags": ["vpn", "browser"] + }, + { + "name": "Firefox โ€” roxxor", + "desc": "Instance Firefox via VPN Gluetun", + "url": "http://192.168.1.29:3200", + "docker": "vpn_mh_roxxor", + "tags": ["vpn", "browser"] + }, + { + "name": "Firefox โ€” xouz", + "desc": "Instance Firefox via VPN Gluetun", + "url": "http://192.168.1.29:3300", + "docker": "vpn_mh_xouz", + "tags": ["vpn", "browser"] + }, + { + "name": "Firefox โ€” nale", + "desc": "Instance Firefox via VPN Gluetun", + "url": "http://192.168.1.29:3400", + "docker": "vpn_mh_nale", + "tags": ["vpn", "browser"] + } + ] + } + ] +}