@php $stackBadge = $site['stack'] === 'laravel' ? 'bg-red-500/10 text-red-300 ring-red-500/20' : 'bg-emerald-500/10 text-emerald-300 ring-emerald-500/20'; // Logo = vrai favicon du site. Prod → service DuckDuckGo (le plus fiable, renvoie // un vrai PNG pour tout domaine) ; local-only → /favicon.ico. Cascade de replis : // DuckDuckGo → /favicon.png → /favicon.ico → monogramme (les SPA renvoient du HTML // sur /favicon.ico, d'où la cascade qui retombe proprement sur le monogramme). $origin = $site['prod_url'] ?: $site['local_url']; $host = parse_url($origin, PHP_URL_HOST); $base = rtrim($origin, '/'); if (! empty($site['prod_url'])) { $faviconPrimary = 'https://icons.duckduckgo.com/ip3/' . $host . '.ico'; $faviconFallb = $base . '/favicon.png,' . $base . '/favicon.ico'; } else { $faviconPrimary = $base . '/favicon.png'; $faviconFallb = $base . '/favicon.ico'; } // Logo dédié posé à la main : public/logos/{clé}.(png|svg|jpg|webp) → prioritaire // sur le favicon auto (utile pour les apps privées/local sans favicon public, ex. Evio). foreach (['png', 'svg', 'jpg', 'jpeg', 'webp'] as $ext) { if (is_file(public_path("logos/{$site['key']}.$ext"))) { $faviconFallb = $faviconPrimary . ',' . $faviconFallb; // l'auto devient un repli $faviconPrimary = asset("logos/{$site['key']}.$ext") . '?v=1'; break; } } $initials = strtoupper(mb_substr(preg_replace('/[^A-Za-z0-9]/u', '', $site['name']) ?: '?', 0, 2)); $palette = ['#6366f1','#0ea5e9','#10b981','#f59e0b','#ef4444','#8b5cf6','#ec4899','#14b8a6','#f97316','#3b82f6']; $logoBg = $palette[abs(crc32($site['key'])) % count($palette)]; $auditable = $site['seo'] ?? true; // Famille / marque (config/familles.php) + rang pour le tri groupé. $famOrder = config('familles.order', []); $famille = config('familles.map.' . $site['key']) ?? 'Divers'; $famRank = array_search($famille, $famOrder); $famRank = $famRank === false ? 99 : $famRank; @endphp
{{-- En-tête : logo + nom + état --}}
{{ $initials }}

{{ $site['name'] }}

{{ $site['stack'] }}
{{ $site['up'] ? 'en ligne · '.$site['status'] : ($site['status'] ? 'erreur '.$site['status'] : 'hors ligne') }} @if (!empty($site['has_leads'])) · … @endif
{{-- Accès aux supports --}}
@if ($site['admin_url']) Admin → @endif Local ↗ @if ($site['prod_url']) Prod ↗ @else Prod — @endif
{{-- Pastilles git : orange = à faire, gris = à jour --}}
{{-- Incidents récents (7 j) : muet quand il n'y a rien à signaler. alerts.json est un journal d'événements : on annonce « il s'est passé quelque chose », l'état réel du site est donné par la pastille ci-dessous. --}} @php $siteAlerts = ($alerts ?? [])[$site['key']] ?? []; $critCount = count(array_filter($siteAlerts, fn ($a) => ($a['level'] ?? '') === 'crit')); $tone = $critCount ? 'text-red-400' : 'text-amber-400'; $icons = ['down' => '🔴', 'ssl' => '🔒', 'seo' => '📉', 'lead' => '📥']; @endphp @if ($siteAlerts)
{{ count($siteAlerts) }} incident{{ count($siteAlerts) > 1 ? 's' : '' }} 7 j{{ $critCount ? ' · ' . $critCount . ' critique' . ($critCount > 1 ? 's' : '') : '' }}
@foreach (array_slice($siteAlerts, 0, 5) as $a)
{{ $icons[$a['type'] ?? ''] ?? '⚠️' }} {{ $a['message'] ?? '' }} {{ \Illuminate\Support\Carbon::parse($a['at'])->format('d/m H\hi') }}
@endforeach @if (count($siteAlerts) > 5) + {{ count($siteAlerts) - 5 }} autre(s) · tout voir › @endif
@endif {{-- Disponibilité : menu déroulant (état + historique des coupures, agent VPS 24/7) --}}
Disponibilité
{{-- Sauvegarde : dernière + historique. Muet si le site n'a rien à sauvegarder (site statique sans base) ; rouge si trop ancienne ou en échec. --}} @php $bkHist = array_reverse(($backups ?? [])[$site['key']] ?? []); // plus récente d'abord $bkLast = $bkHist[0] ?? null; // diffInHours() renvoie un FLOTTANT (Carbon 3) : sans cast, l'écran // affichait « il y a 2.2240379897222 h ». $bkAge = $bkLast && ! empty($bkLast['at']) ? (int) \Illuminate\Support\Carbon::parse($bkLast['at'])->diffInHours(now()) : null; $bkBad = $bkLast && (! ($bkLast['ok'] ?? true) || ($bkAge !== null && $bkAge > 48)); $bkTone = $bkBad ? 'text-red-400' : 'text-slate-500'; $bkDot = $bkBad ? 'bg-red-500' : 'bg-emerald-500'; @endphp @if ($bkLast)
Sauvegarde {{ $bkAge !== null ? ($bkAge < 1 ? "à l'instant" : ($bkAge < 24 ? "il y a {$bkAge} h" : 'il y a ' . intdiv($bkAge, 24) . ' j')) : '—' }} @if (! empty($bkLast['size_mb'])) · {{ $bkLast['size_mb'] }} Mo @endif
@foreach (array_slice($bkHist, 0, 6) as $b)
{{ ($b['ok'] ?? true) ? '✅' : '❌' }} {{ \Illuminate\Support\Carbon::parse($b['at'])->format('d/m H\hi') }} {{ ! empty($b['size_mb']) ? $b['size_mb'] . ' Mo' : '' }} @if (! empty($b['items'])) {{ implode(', ', $b['items']) }} @endif @if (! empty($b['error'])) {{ $b['error'] }} @endif
@endforeach @if (! empty($bkLast['server']))
serveur : {{ $bkLast['server'] }}
@endif
@endif {{-- Rapport mensuel du site (généré par hub:monthly-report) --}} @php $siteReports = ($reports ?? [])[$site['key']] ?? []; @endphp @if ($siteReports)
Rapport {{ \Illuminate\Support\Carbon::createFromFormat('Y-m', preg_replace('/^mensuel-.+-(\d{4}-\d{2})\.html$/', '$1', $siteReports[0]))->translatedFormat('F Y') }} › @if (count($siteReports) > 1) +{{ count($siteReports) - 1 }} précédent(s) @endif
@endif {{-- Pied : adresses (publique + locale) + note d'audit --}}
@if ($auditable) @else app privée @endif