homelab_automation/app/index.html
2025-12-04 09:04:42 -05:00

2540 lines
113 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Homelab Automation Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/js/splide.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/css/splide.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary-bg: #0a0a0a;
--secondary-bg: #1a1a1a;
--accent-bg: #2a2a2a;
--primary-text: #ffffff;
--secondary-text: #a1a1aa;
--accent-color: #7c3aed;
--accent-hover: #8b5cf6;
--success-color: #10b981;
--warning-color: #f59e0b;
--error-color: #ef4444;
--border-color: #374151;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: var(--primary-bg);
color: var(--primary-text);
min-height: 100vh;
overflow-x: hidden;
}
.hero-section {
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%);
position: relative;
overflow: hidden;
}
.hero-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 30% 50%, rgba(124, 58, 237, 0.1) 0%, transparent 50%);
pointer-events: none;
}
.glass-card {
background: rgba(42, 42, 42, 0.8);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
transition: all 0.3s ease;
}
.glass-card:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
border-color: rgba(124, 58, 237, 0.3);
}
.gradient-text {
background: linear-gradient(135deg, #7c3aed 0%, #3b82f6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.status-online { background-color: var(--success-color); }
.status-offline { background-color: var(--error-color); }
.status-warning { background-color: var(--warning-color); }
.animate-float {
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
.animate-pulse-slow {
animation: pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-hover) 100%);
border: none;
padding: 12px 24px;
border-radius: 8px;
color: white;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(124, 58, 237, 0.3);
}
.btn-primary:active {
transform: translateY(0);
}
.metric-card {
background: rgba(42, 42, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 24px;
text-align: center;
transition: all 0.3s ease;
}
.metric-card:hover {
background: rgba(42, 42, 42, 0.8);
transform: translateY(-2px);
}
.host-card {
background: rgba(42, 42, 42, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
transition: all 0.3s ease;
cursor: pointer;
}
.host-card:hover {
background: rgba(42, 42, 42, 0.8);
border-color: rgba(124, 58, 237, 0.3);
transform: translateX(4px);
}
.log-entry {
background: rgba(0, 0, 0, 0.3);
border-left: 3px solid var(--accent-color);
padding: 12px 16px;
margin-bottom: 8px;
border-radius: 0 8px 8px 0;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.splide__slide {
display: flex;
align-items: center;
justify-content: center;
}
.feature-icon {
width: 64px;
height: 64px;
background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-hover) 100%);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
font-size: 28px;
color: white;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top: 3px solid var(--accent-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.fade-in {
opacity: 0;
transform: translateY(30px);
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
transition: all 0.6s ease;
}
.grid-pattern {
background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.1) 1px, transparent 0);
background-size: 20px 20px;
}
/* Styles pour les boutons de filtrage des tâches */
.task-filter-btn {
position: relative;
overflow: hidden;
}
.task-filter-btn.active {
box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3);
}
.task-filter-btn[data-status="running"].active {
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3);
}
.task-filter-btn[data-status="completed"].active {
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
}
.task-filter-btn[data-status="failed"].active {
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
}
/* Animation pour les cards de tâches */
.task-log-card {
transition: all 0.2s ease;
}
.task-log-card:hover {
transform: translateX(4px);
}
/* Modal responsive */
#modal .glass-card {
max-width: 1200px;
width: 95%;
max-height: 90vh;
overflow-y: auto;
}
@media (max-width: 1024px) {
#modal .glass-card {
max-width: 95%;
}
}
/* Modal Ad-Hoc spécifique - plus large et plus haut */
#modal .adhoc-modal {
max-width: 1400px;
width: 98%;
max-height: 92vh;
}
/* Layout de la console Ad-Hoc */
.adhoc-console-content {
min-height: 500px;
}
/* Section des warnings collapsible */
#adhoc-stderr-section details summary::-webkit-details-marker {
display: none;
}
#adhoc-stderr-section details[open] summary .fa-chevron-down {
transform: rotate(180deg);
}
/* Host tabs styling */
.host-tab {
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.host-tab:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* Category filter buttons */
.category-filter-btn {
cursor: pointer;
}
.category-filter-btn.active {
font-weight: 600;
}
.category-filter-btn:not(.active):hover {
filter: brightness(1.2);
}
/* History section animation */
.history-category-section {
transition: all 0.2s ease;
}
.history-category-section.hidden {
display: none;
}
/* Ad-Hoc Console Styles */
#adhoc-result {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#adhoc-stdout, #adhoc-stderr {
scrollbar-width: thin;
scrollbar-color: rgba(124, 58, 237, 0.5) rgba(0, 0, 0, 0.3);
}
#adhoc-stdout::-webkit-scrollbar,
#adhoc-stderr::-webkit-scrollbar {
width: 8px;
height: 8px;
}
#adhoc-stdout::-webkit-scrollbar-track,
#adhoc-stderr::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
#adhoc-stdout::-webkit-scrollbar-thumb,
#adhoc-stderr::-webkit-scrollbar-thumb {
background: rgba(124, 58, 237, 0.5);
border-radius: 4px;
}
#adhoc-stdout::-webkit-scrollbar-thumb:hover,
#adhoc-stderr::-webkit-scrollbar-thumb:hover {
background: rgba(124, 58, 237, 0.7);
}
/* Amélioration de la lisibilité du code */
#adhoc-stdout pre, #adhoc-stderr pre,
#adhoc-stdout, #adhoc-stderr {
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
font-size: 13px;
line-height: 1.6;
tab-size: 4;
}
/* Animation pour le loading */
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Task Log Viewer Styles */
.task-log-viewer {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.tasklog-host-tab {
transition: all 0.2s ease;
}
.tasklog-host-tab:hover {
transform: translateY(-1px);
}
.tasklog-host-tab.active {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
#tasklog-output {
scrollbar-width: thin;
scrollbar-color: rgba(124, 58, 237, 0.5) rgba(0, 0, 0, 0.3);
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
font-size: 13px;
line-height: 1.6;
tab-size: 4;
}
#tasklog-output::-webkit-scrollbar {
width: 8px;
height: 8px;
}
#tasklog-output::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
#tasklog-output::-webkit-scrollbar-thumb {
background: rgba(124, 58, 237, 0.5);
border-radius: 4px;
}
#tasklog-output::-webkit-scrollbar-thumb:hover {
background: rgba(124, 58, 237, 0.7);
}
/* Modal task log specific - larger modal */
#modal .task-log-viewer {
max-width: 100%;
}
@media (min-width: 1024px) {
#modal:has(.task-log-viewer) .glass-card {
max-width: 1100px;
width: 95%;
}
}
/* Details/Summary styles for errors section */
.task-log-viewer details summary::-webkit-details-marker {
display: none;
}
.task-log-viewer details[open] summary .fa-chevron-down {
transform: rotate(180deg);
}
/* ===== ANSIBLE VIEWER STYLES ===== */
.ansible-viewer {
animation: fadeIn 0.3s ease-out;
}
.playbook-execution-viewer {
min-width: 800px;
}
@media (max-width: 1024px) {
.playbook-execution-viewer {
min-width: auto;
}
}
/* Modal plus large pour le viewer */
#modal:has(.ansible-viewer) .glass-card,
#modal:has(.playbook-execution-viewer) .glass-card {
max-width: 1200px;
width: 95%;
}
/* Pas de scroll sur le viewer global */
.playbook-execution-viewer,
.ansible-viewer {
overflow: visible;
}
/* Execution Header */
.execution-header {
backdrop-filter: blur(10px);
}
/* Host Cards */
.host-card-item {
transition: all 0.2s ease;
backdrop-filter: blur(5px);
}
.host-card-item:hover {
transform: scale(1.02);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
/* Stat Cards */
.stat-card {
backdrop-filter: blur(5px);
transition: all 0.2s ease;
}
.stat-card:hover {
transform: translateY(-2px);
}
/* Task Hierarchy - scroll interne dans la boîte PLAY */
.task-tree {
max-height: 400px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(124, 58, 237, 0.5) rgba(0, 0, 0, 0.3);
}
.task-tree::-webkit-scrollbar {
width: 6px;
}
.task-tree::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
}
.task-tree::-webkit-scrollbar-thumb {
background: rgba(124, 58, 237, 0.5);
border-radius: 3px;
}
.task-item summary {
list-style: none;
}
.task-item summary::-webkit-details-marker {
display: none;
}
.task-item[open] summary .fa-chevron-right {
transform: rotate(90deg);
}
.task-item summary .fa-chevron-right {
transition: transform 0.2s ease;
}
/* Play Section */
.play-section {
border-bottom: 1px solid rgba(55, 65, 81, 0.5);
}
.play-section:last-child {
border-bottom: none;
}
/* Host Result Items */
.host-result {
transition: all 0.15s ease;
}
.host-result:hover {
background: rgba(124, 58, 237, 0.1);
}
/* Filter Buttons */
.av-filter-btn {
transition: all 0.2s ease;
}
.av-filter-btn.active {
box-shadow: 0 0 10px rgba(124, 58, 237, 0.3);
}
/* Progress bars in host cards */
.host-card-item .flex.gap-1 > div {
transition: width 0.5s ease;
}
/* Animations for stats */
@keyframes countUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.stat-card .text-2xl {
animation: countUp 0.5s ease-out;
}
/* Raw output section */
#ansible-raw-output {
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
font-size: 12px;
line-height: 1.5;
}
/* Host details modal */
.host-details-modal .tasks-list {
scrollbar-width: thin;
scrollbar-color: rgba(124, 58, 237, 0.5) rgba(0, 0, 0, 0.3);
}
.host-details-modal .tasks-list::-webkit-scrollbar {
width: 6px;
}
.host-details-modal .tasks-list::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
.host-details-modal .tasks-list::-webkit-scrollbar-thumb {
background: rgba(124, 58, 237, 0.5);
border-radius: 3px;
}
/* ===== LIGHT THEME STYLES ===== */
body.light-theme {
--primary-bg: #f5f5f5;
--secondary-bg: #ffffff;
--accent-bg: #e5e5e5;
--primary-text: #1a1a1a;
--secondary-text: #6b7280;
--border-color: #d1d5db;
}
body.light-theme {
background: var(--primary-bg);
color: var(--primary-text);
}
body.light-theme .hero-section {
background: linear-gradient(135deg, #f5f5f5 0%, #e5e5e5 100%);
}
body.light-theme .glass-card {
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.1);
}
body.light-theme .metric-card {
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(0, 0, 0, 0.1);
}
body.light-theme .host-card {
background: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(0, 0, 0, 0.1);
}
body.light-theme .host-card:hover {
background: rgba(255, 255, 255, 0.9);
}
body.light-theme nav {
background: rgba(255, 255, 255, 0.9) !important;
border-color: #e5e5e5 !important;
}
body.light-theme nav a,
body.light-theme nav h1 {
color: #1a1a1a !important;
}
body.light-theme nav a:hover {
color: #7c3aed !important;
}
body.light-theme .text-white {
color: #1a1a1a !important;
}
body.light-theme .text-gray-300,
body.light-theme .text-gray-400,
body.light-theme .text-gray-500 {
color: #6b7280 !important;
}
body.light-theme .bg-gray-700,
body.light-theme .bg-gray-800 {
background-color: #e5e5e5 !important;
}
body.light-theme .bg-black {
background-color: #f5f5f5 !important;
}
body.light-theme #modal .glass-card {
background: rgba(255, 255, 255, 0.95);
}
body.light-theme .log-entry {
background: rgba(0, 0, 0, 0.05);
}
body.light-theme footer {
background: #f5f5f5 !important;
border-color: #e5e5e5 !important;
}
body.light-theme select,
body.light-theme input {
background-color: #ffffff !important;
border-color: #d1d5db !important;
color: #1a1a1a !important;
}
body.light-theme .task-filter-btn:not(.active) {
background-color: #e5e5e5 !important;
color: #1a1a1a !important;
}
body.light-theme pre {
background-color: #1a1a1a !important;
}
/* Thème clair - calendrier de filtrage des tâches */
body.light-theme .task-calendar {
background-color: #ffffff !important;
border-color: #d1d5db !important;
color: #111827 !important;
}
body.light-theme #task-date-filter-button {
background-color: #111827 !important;
border-color: #374151 !important;
color: #f9fafb !important;
}
body.light-theme #task-date-filter-button:hover {
background-color: #1f2937 !important;
}
body.light-theme #task-cal-grid button {
color: #111827 !important;
}
body.light-theme #task-cal-grid button.bg-purple-600 {
color: #f9fafb !important;
}
body.light-theme #task-cal-summary {
color: #4b5563 !important;
}
body.light-theme #task-cal-clear {
background-color: #e5e7eb !important;
border-color: #d1d5db !important;
color: #111827 !important;
}
body.light-theme #task-cal-clear:hover {
background-color: #d1d5db !important;
}
body.light-theme #task-cal-apply {
background-color: #7c3aed !important;
color: #f9fafb !important;
}
/* Navigation active state */
.nav-link.active {
color: #7c3aed !important;
font-weight: 600;
border-bottom: 2px solid #7c3aed;
padding-bottom: 2px;
}
/* Page transitions */
.page-section {
display: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.page-section.active {
display: block;
opacity: 1;
}
/* Help page specific styles */
.help-card {
background: rgba(42, 42, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 24px;
transition: all 0.3s ease;
}
.help-card:hover {
background: rgba(42, 42, 42, 0.8);
border-color: rgba(124, 58, 237, 0.3);
}
.help-section-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.help-list {
list-style: none;
padding: 0;
}
.help-list li {
padding: 0.5rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.help-list li:last-child {
border-bottom: none;
}
.help-code {
background: rgba(0, 0, 0, 0.4);
padding: 2px 8px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
}
.accordion-item {
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
margin-bottom: 8px;
overflow: hidden;
}
.accordion-header {
background: rgba(42, 42, 42, 0.6);
padding: 16px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.2s ease;
}
.accordion-header:hover {
background: rgba(42, 42, 42, 0.8);
}
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
background: rgba(26, 26, 26, 0.6);
}
.accordion-item.open .accordion-content {
max-height: 1000px;
}
.accordion-item.open .accordion-icon {
transform: rotate(180deg);
}
.accordion-icon {
transition: transform 0.3s ease;
}
/* ===== TABLE DES MATIERES AIDE ===== */
.help-toc {
position: sticky;
top: 100px;
max-height: calc(100vh - 120px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(124, 58, 237, 0.5) rgba(0, 0, 0, 0.3);
}
.help-toc::-webkit-scrollbar {
width: 4px;
}
.help-toc::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
border-radius: 2px;
}
.help-toc::-webkit-scrollbar-thumb {
background: rgba(124, 58, 237, 0.5);
border-radius: 2px;
}
.help-toc-item {
display: block;
padding: 8px 12px;
color: #9ca3af;
text-decoration: none;
font-size: 0.875rem;
border-left: 2px solid transparent;
transition: all 0.2s ease;
}
.help-toc-item:hover {
color: #e5e7eb;
background: rgba(124, 58, 237, 0.1);
border-left-color: rgba(124, 58, 237, 0.5);
}
.help-toc-item.active {
color: #a78bfa;
border-left-color: #7c3aed;
background: rgba(124, 58, 237, 0.15);
}
.help-toc-title {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6b7280;
padding: 12px 12px 8px;
}
.help-main-content {
scroll-behavior: smooth;
}
.help-section-anchor {
scroll-margin-top: 100px;
}
/* Indicateur de santé visuel */
.health-indicator-demo {
display: flex;
align-items: center;
gap: 4px;
}
.health-bar {
width: 6px;
height: 16px;
border-radius: 2px;
transition: background-color 0.3s ease;
}
body.light-theme .help-toc {
background: rgba(255, 255, 255, 0.9);
}
body.light-theme .help-toc-item {
color: #4b5563;
}
body.light-theme .help-toc-item:hover {
color: #1f2937;
background: rgba(124, 58, 237, 0.1);
}
body.light-theme .help-toc-item.active {
color: #7c3aed;
}
/* Ajustements supplémentaires thème clair pour un meilleur contraste */
body.light-theme #dashboard {
background: linear-gradient(180deg, #f9fafb 0%, #e5e7eb 100%);
}
body.light-theme #tasks {
background: #f9fafb;
}
body.light-theme #logs {
background: linear-gradient(180deg, #f9fafb 0%, #e5e7eb 100%);
}
/* Badges "Ready" / "Non configuré" en haut du bloc Hosts */
body.light-theme .bg-green-600\/20 {
background-color: #bbf7d0 !important; /* vert clair plein */
color: #166534 !important; /* vert foncé lisible */
}
body.light-theme .bg-yellow-600\/20 {
background-color: #fef3c7 !important; /* jaune clair plein */
color: #92400e !important; /* texte brun foncé */
}
/* Badges Ansible Ready / Non configuré dans les cartes d'hôtes */
body.light-theme .bg-green-600\/30 {
background-color: #a7f3d0 !important;
color: #166534 !important;
}
body.light-theme .bg-yellow-600\/30 {
background-color: #fde68a !important;
color: #92400e !important;
}
/* Barre de filtres de date dans la section Tâches */
body.light-theme .bg-gray-800\/50 {
background-color: #e5e7eb !important;
}
body.light-theme .bg-gray-600 {
background-color: #9ca3af !important;
color: #111827 !important;
}
body.light-theme .bg-purple-600 {
color: #f9fafb !important;
}
/* Viewer de logs Ansible (modal) en thème clair */
body.light-theme .playbook-execution-viewer {
background-color: #f9fafb;
}
body.light-theme .playbook-execution-viewer .task-hierarchy-section .task-tree {
background-color: #e5e7eb !important;
border-color: #d1d5db !important;
}
body.light-theme .playbook-execution-viewer .task-tree .host-card-item {
background-color: #f3f4f6 !important;
border-color: #d1d5db !important;
}
body.light-theme .playbook-execution-viewer .task-tree .host-card-item:hover {
background-color: #e5e7eb !important;
}
/* Fond sombre semi-transparent utilisé dans le viewer */
body.light-theme .playbook-execution-viewer .bg-gray-900\/50 {
background-color: #e5e7eb !important;
}
/* Titres des tâches dans la hiérarchie (résultats playbook) */
body.light-theme .playbook-execution-viewer .task-item summary .text-gray-200 {
color: #111827 !important; /* texte presque noir sur fond clair */
}
body.light-theme .playbook-execution-viewer .play-header {
background-color: #d1d5db !important;
border-color: #9ca3af !important;
}
/* Scrollbar de la hiérarchie des tâches plus grise que mauve */
body.light-theme .playbook-execution-viewer .task-tree {
scrollbar-width: thin;
scrollbar-color: #9ca3af #e5e7eb; /* pouce gris sur fond gris clair */
}
body.light-theme .playbook-execution-viewer .task-tree::-webkit-scrollbar {
width: 8px;
}
body.light-theme .playbook-execution-viewer .task-tree::-webkit-scrollbar-track {
background: #e5e7eb;
border-radius: 4px;
}
body.light-theme .playbook-execution-viewer .task-tree::-webkit-scrollbar-thumb {
background: #9ca3af;
border-radius: 4px;
}
/* ===== PLAYBOOKS PAGE STYLES ===== */
.playbook-card {
background: rgba(42, 42, 42, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 20px;
transition: all 0.2s ease;
cursor: pointer;
}
.playbook-card:hover {
background: rgba(42, 42, 42, 0.8);
border-color: rgba(139, 92, 246, 0.5);
transform: translateX(4px);
box-shadow: 0 4px 20px rgba(139, 92, 246, 0.15);
}
.playbook-card.selected {
border-color: #8b5cf6;
background: rgba(139, 92, 246, 0.1);
}
.playbook-category-badge {
font-size: 0.75rem;
padding: 4px 10px;
border-radius: 9999px;
font-weight: 500;
background-color: rgba(75, 85, 99, 0.35);
color: #e5e7eb;
border: 1px solid rgba(75, 85, 99, 0.75);
}
.playbook-category-maintenance {
background-color: rgba(249, 115, 22, 0.2);
color: #fb923c;
}
.playbook-category-deploy {
background-color: rgba(59, 130, 246, 0.2);
color: #60a5fa;
}
.playbook-category-backup {
background-color: rgba(34, 197, 94, 0.2);
color: #4ade80;
}
.playbook-category-system {
background-color: rgba(168, 85, 247, 0.2);
color: #c084fc;
}
.playbook-category-monitoring {
background-color: rgba(20, 184, 166, 0.2);
color: #2dd4bf;
}
.playbook-category-general {
background-color: rgba(156, 163, 175, 0.2);
color: #9ca3af;
}
/* Monaco Editor Modal */
.playbook-editor-modal {
max-width: 1400px !important;
width: 98% !important;
max-height: 92vh !important;
}
.playbook-editor-container {
height: 60vh;
min-height: 400px;
border: 1px solid #374151;
border-radius: 8px;
overflow: hidden;
}
/* Simple code textarea fallback */
.playbook-code-editor {
width: 100%;
height: 100%;
min-height: 400px;
background: #1e1e1e;
color: #d4d4d4;
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
font-size: 14px;
line-height: 1.6;
padding: 16px;
border: none;
resize: none;
outline: none;
tab-size: 2;
}
.playbook-code-editor:focus {
outline: none;
box-shadow: inset 0 0 0 2px rgba(139, 92, 246, 0.3);
}
/* Syntax highlighting hint text */
.yaml-hint {
background: rgba(139, 92, 246, 0.1);
border: 1px solid rgba(139, 92, 246, 0.3);
border-radius: 6px;
padding: 8px 12px;
font-size: 0.75rem;
color: #a78bfa;
}
/* Editor toolbar */
.editor-toolbar {
background: rgba(42, 42, 42, 0.8);
border-bottom: 1px solid #374151;
padding: 12px 16px;
}
/* File size indicator */
.file-size-badge {
font-size: 0.7rem;
padding: 2px 8px;
background: rgba(107, 114, 128, 0.3);
color: #9ca3af;
border-radius: 4px;
}
/* Time ago styling */
.time-ago {
color: #6b7280;
font-size: 0.8rem;
}
/* Playbook action buttons */
.playbook-action-btn {
padding: 6px;
border-radius: 6px;
transition: all 0.15s ease;
}
.playbook-action-btn:hover {
transform: scale(1.1);
}
.playbook-action-btn.edit {
background: rgba(59, 130, 246, 0.2);
color: #60a5fa;
}
.playbook-action-btn.edit:hover {
background: rgba(59, 130, 246, 0.4);
}
.playbook-action-btn.run {
background: rgba(34, 197, 94, 0.2);
color: #4ade80;
}
.playbook-action-btn.run:hover {
background: rgba(34, 197, 94, 0.4);
}
.playbook-action-btn.delete {
background: rgba(239, 68, 68, 0.2);
color: #f87171;
}
.playbook-action-btn.delete:hover {
background: rgba(239, 68, 68, 0.4);
}
/* Search input */
.playbook-search {
background: rgba(17, 24, 39, 0.8);
border: 1px solid #374151;
border-radius: 8px;
padding: 10px 16px 10px 40px;
color: white;
transition: all 0.2s ease;
}
.playbook-search:focus {
border-color: #8b5cf6;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
outline: none;
}
.playbook-search::placeholder {
color: #6b7280;
}
/* Light theme overrides for playbooks */
body.light-theme .playbook-card {
background: rgba(255, 255, 255, 0.6);
border-color: #d1d5db;
}
body.light-theme .playbook-card:hover {
background: rgba(255, 255, 255, 0.9);
}
body.light-theme .playbook-code-editor {
background: #1e1e1e;
color: #d4d4d4;
}
body.light-theme .playbook-search {
background: #ffffff;
border-color: #d1d5db;
color: #111827;
}
</style>
</head>
<body>
<!-- Navigation Header -->
<nav class="fixed top-0 left-0 right-0 z-50 bg-black bg-opacity-80 backdrop-blur-lg border-b border-gray-800">
<div class="max-w-7xl mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-gradient-to-br from-purple-600 to-blue-600 rounded-lg flex items-center justify-center">
<i class="fas fa-server text-white text-lg"></i>
</div>
<h1 class="text-xl font-bold gradient-text">Homelab Dashboard</h1>
</div>
<div class="flex items-center space-x-6">
<a href="#" data-page="dashboard" class="nav-link text-gray-300 hover:text-white transition-colors">Dashboard</a>
<a href="#" data-page="hosts" class="nav-link text-gray-300 hover:text-white transition-colors">Hosts</a>
<a href="#" data-page="playbooks" class="nav-link text-gray-300 hover:text-white transition-colors">Playbooks</a>
<a href="#" data-page="tasks" class="nav-link text-gray-300 hover:text-white transition-colors">Tasks</a>
<a href="#" data-page="logs" class="nav-link text-gray-300 hover:text-white transition-colors">Logs</a>
<a href="#" data-page="help" class="nav-link text-gray-300 hover:text-white transition-colors">Aide</a>
<button id="theme-toggle" class="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors">
<i class="fas fa-moon text-gray-300"></i>
</button>
</div>
</div>
</div>
</nav>
<!-- ==================== PAGE: DASHBOARD ==================== -->
<section id="page-dashboard" class="page-section active">
<!-- Hero Section -->
<div class="hero-section pt-24 pb-16">
<div class="max-w-7xl mx-auto px-6">
<div class="text-center mb-16">
<h1 class="text-5xl font-bold mb-6 gradient-text animate-float">
Automation Dashboard
</h1>
<p class="text-xl text-gray-400 mb-8 max-w-2xl mx-auto">
Gérez votre homelab avec puissance et élégance. Surveillance, automatisation et contrôle en temps réel.
</p>
<div class="flex justify-center space-x-4">
<button class="btn-primary" onclick="dashboard.showQuickActions()">
<i class="fas fa-bolt mr-2"></i>
Actions Rapides
</button>
<button class="px-6 py-3 border border-gray-600 rounded-lg text-gray-300 hover:border-purple-500 hover:text-white transition-all" onclick="navigateTo('hosts')">
<i class="fas fa-server mr-2"></i>
Voir les Hosts
</button>
</div>
</div>
<!-- Metrics Overview -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-16">
<div class="metric-card fade-in">
<div class="text-3xl font-bold text-green-400 mb-2" id="online-hosts">12</div>
<div class="text-gray-400">Hosts En Ligne</div>
</div>
<div class="metric-card fade-in">
<div class="text-3xl font-bold text-blue-400 mb-2" id="total-tasks">48</div>
<div class="text-gray-400">Tâches Exécutées</div>
</div>
<div class="metric-card fade-in">
<div class="text-3xl font-bold text-purple-400 mb-2" id="success-rate">98.5%</div>
<div class="text-gray-400">Taux de Succès</div>
</div>
<div class="metric-card fade-in">
<div class="text-3xl font-bold text-orange-400 mb-2" id="uptime">99.9%</div>
<div class="text-gray-400">Disponibilité</div>
</div>
</div>
</div>
</div>
<!-- Dashboard Content -->
<div id="dashboard" class="py-16 bg-gradient-to-b from-gray-900 to-black">
<div class="max-w-7xl mx-auto px-6">
<h2 class="text-3xl font-bold mb-12 text-center gradient-text fade-in">Tableau de Bord</h2>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Hosts Management -->
<div class="lg:col-span-2">
<div class="glass-card p-6 fade-in">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-semibold">Gestion des Hosts</h3>
<div class="flex items-center space-x-2">
<button class="btn-primary" onclick="dashboard.addHost()">
<i class="fas fa-plus mr-2"></i>
Ajouter Host
</button>
<div class="relative group">
<button class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors flex items-center">
<i class="fas fa-layer-group mr-2"></i>
Groupes
<i class="fas fa-chevron-down ml-2 text-xs"></i>
</button>
<div class="absolute right-0 mt-2 w-56 bg-gray-800 border border-gray-700 rounded-lg shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
<div class="py-2">
<div class="px-4 py-2 text-xs text-gray-400 uppercase tracking-wider border-b border-gray-700">Environnements</div>
<button onclick="dashboard.showAddGroupModal('env')" class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center">
<i class="fas fa-plus text-green-400 mr-3 w-4"></i>
Ajouter environnement
</button>
<button onclick="dashboard.showManageGroupsModal('env')" class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center">
<i class="fas fa-edit text-blue-400 mr-3 w-4"></i>
Gérer environnements
</button>
<div class="px-4 py-2 text-xs text-gray-400 uppercase tracking-wider border-b border-t border-gray-700 mt-2">Rôles</div>
<button onclick="dashboard.showAddGroupModal('role')" class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center">
<i class="fas fa-plus text-green-400 mr-3 w-4"></i>
Ajouter rôle
</button>
<button onclick="dashboard.showManageGroupsModal('role')" class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center">
<i class="fas fa-edit text-blue-400 mr-3 w-4"></i>
Gérer rôles
</button>
</div>
</div>
</div>
</div>
</div>
<div id="hosts-list" class="space-y-4">
<!-- Hosts will be populated by JavaScript -->
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="glass-card p-6 fade-in">
<h3 class="text-xl font-semibold mb-6">Actions Rapides</h3>
<div class="space-y-4">
<button class="w-full p-4 bg-gradient-to-r from-green-600 to-green-700 rounded-lg text-left hover:from-green-500 hover:to-green-600 transition-all" onclick="executeTask('upgrade-all')">
<i class="fas fa-arrow-up mr-3"></i>
Mettre à jour tous les hôtes
</button>
<button class="w-full p-4 bg-gradient-to-r from-blue-600 to-blue-700 rounded-lg text-left hover:from-blue-500 hover:to-blue-600 transition-all" onclick="executeTask('reboot-all')">
<i class="fas fa-redo mr-3"></i>
Redémarrer les hôtes
</button>
<button class="w-full p-4 bg-gradient-to-r from-purple-600 to-purple-700 rounded-lg text-left hover:from-purple-500 hover:to-purple-600 transition-all" onclick="executeTask('health-check')">
<i class="fas fa-heartbeat mr-3"></i>
Vérifier la santé
</button>
<button class="w-full p-4 bg-gradient-to-r from-orange-600 to-orange-700 rounded-lg text-left hover:from-orange-500 hover:to-orange-600 transition-all" onclick="executeTask('backup')">
<i class="fas fa-save mr-3"></i>
Sauvegarder la configuration
</button>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- END PAGE: DASHBOARD -->
<!-- ==================== PAGE: HOSTS ==================== -->
<section id="page-hosts" class="page-section">
<div class="pt-24 pb-16 min-h-screen bg-gradient-to-b from-gray-900 to-black">
<div class="max-w-7xl mx-auto px-6">
<div class="text-center mb-12">
<h1 class="text-4xl font-bold mb-4 gradient-text">
<i class="fas fa-server mr-3"></i>Gestion des Hosts
</h1>
<p class="text-gray-400">Gérez et surveillez tous vos serveurs depuis un seul endroit</p>
</div>
<div class="glass-card p-6 fade-in">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-semibold">Inventaire des Hosts</h3>
<div class="flex items-center space-x-2">
<button class="btn-primary" onclick="dashboard.addHost()">
<i class="fas fa-plus mr-2"></i>
Ajouter Host
</button>
<div class="relative group">
<button class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors flex items-center">
<i class="fas fa-layer-group mr-2"></i>
Groupes
<i class="fas fa-chevron-down ml-2 text-xs"></i>
</button>
<div class="absolute right-0 mt-2 w-56 bg-gray-800 border border-gray-700 rounded-lg shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
<div class="py-2">
<div class="px-4 py-2 text-xs text-gray-400 uppercase tracking-wider border-b border-gray-700">Environnements</div>
<button onclick="dashboard.showAddGroupModal('env')" class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center">
<i class="fas fa-plus text-green-400 mr-3 w-4"></i>
Ajouter environnement
</button>
<button onclick="dashboard.showManageGroupsModal('env')" class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center">
<i class="fas fa-edit text-blue-400 mr-3 w-4"></i>
Gérer environnements
</button>
<div class="px-4 py-2 text-xs text-gray-400 uppercase tracking-wider border-b border-t border-gray-700 mt-2">Rôles</div>
<button onclick="dashboard.showAddGroupModal('role')" class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center">
<i class="fas fa-plus text-green-400 mr-3 w-4"></i>
Ajouter rôle
</button>
<button onclick="dashboard.showManageGroupsModal('role')" class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center">
<i class="fas fa-edit text-blue-400 mr-3 w-4"></i>
Gérer rôles
</button>
</div>
</div>
</div>
</div>
</div>
<div id="hosts-page-list" class="space-y-4">
<!-- Hosts will be populated by JavaScript - synced with hosts-list -->
</div>
</div>
</div>
</div>
</section>
<!-- END PAGE: HOSTS -->
<!-- ==================== PAGE: PLAYBOOKS ==================== -->
<section id="page-playbooks" class="page-section">
<div class="pt-24 pb-16 min-h-screen bg-gradient-to-b from-gray-900 to-black">
<div class="max-w-7xl mx-auto px-6">
<!-- Header avec effet glow violet -->
<div class="text-center mb-12">
<h1 class="text-4xl font-bold mb-4 gradient-text">
<i class="fas fa-book mr-3"></i>Gestion des Playbooks
</h1>
<p class="text-gray-400 max-w-2xl mx-auto">Gérez vos scripts d'automatisation Ansible depuis un seul endroit</p>
</div>
<div class="glass-card p-6 fade-in">
<!-- Toolbar -->
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4 mb-6">
<!-- Gauche: Recherche et compteur -->
<div class="flex items-center gap-4">
<div class="relative">
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
<input type="text"
id="playbook-search-input"
class="playbook-search w-64"
placeholder="Rechercher un playbook..."
oninput="dashboard.filterPlaybooks(this.value)">
</div>
<span id="playbooks-count" class="text-sm text-gray-400">
<i class="fas fa-file-code mr-1"></i>0 playbooks
</span>
</div>
<!-- Droite: Boutons d'action -->
<div class="flex items-center gap-3">
<button onclick="dashboard.refreshPlaybooks()"
class="px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors text-sm flex items-center gap-2">
<i class="fas fa-sync-alt"></i>
<span class="hidden sm:inline">Rafraîchir</span>
</button>
<button onclick="dashboard.showCreatePlaybookModal()"
class="px-4 py-2 bg-purple-600 rounded-lg hover:bg-purple-500 transition-colors text-sm flex items-center gap-2 shadow-lg shadow-purple-500/20">
<i class="fas fa-plus"></i>
<span>Nouveau Playbook</span>
</button>
</div>
</div>
<!-- Filtres par catégorie -->
<div id="playbook-category-filters" class="flex flex-wrap items-center gap-2 mb-6 p-3 bg-gray-800/50 rounded-lg">
<span class="text-xs text-gray-500 mr-2"><i class="fas fa-filter mr-1"></i>Catégorie:</span>
<button onclick="dashboard.filterPlaybooksByCategory('all')"
class="playbook-filter-btn px-3 py-1.5 text-xs rounded-lg transition-colors bg-purple-600 text-white" data-category="all">
Tous
</button>
<button onclick="dashboard.filterPlaybooksByCategory('maintenance')"
class="playbook-filter-btn px-3 py-1.5 text-xs rounded-lg transition-colors bg-gray-700 text-gray-300 hover:bg-gray-600" data-category="maintenance">
<i class="fas fa-wrench mr-1"></i>Maintenance
</button>
<button onclick="dashboard.filterPlaybooksByCategory('deploy')"
class="playbook-filter-btn px-3 py-1.5 text-xs rounded-lg transition-colors bg-gray-700 text-gray-300 hover:bg-gray-600" data-category="deploy">
<i class="fas fa-rocket mr-1"></i>Deploy
</button>
<button onclick="dashboard.filterPlaybooksByCategory('backup')"
class="playbook-filter-btn px-3 py-1.5 text-xs rounded-lg transition-colors bg-gray-700 text-gray-300 hover:bg-gray-600" data-category="backup">
<i class="fas fa-save mr-1"></i>Backup
</button>
<button onclick="dashboard.filterPlaybooksByCategory('monitoring')"
class="playbook-filter-btn px-3 py-1.5 text-xs rounded-lg transition-colors bg-gray-700 text-gray-300 hover:bg-gray-600" data-category="monitoring">
<i class="fas fa-heartbeat mr-1"></i>Monitoring
</button>
<button onclick="dashboard.filterPlaybooksByCategory('system')"
class="playbook-filter-btn px-3 py-1.5 text-xs rounded-lg transition-colors bg-gray-700 text-gray-300 hover:bg-gray-600" data-category="system">
<i class="fas fa-cogs mr-1"></i>System
</button>
</div>
<!-- Liste des Playbooks -->
<div id="playbooks-list" class="space-y-3">
<!-- Playbooks will be populated by JavaScript -->
<div class="text-center py-12 text-gray-500">
<i class="fas fa-spinner fa-spin text-3xl mb-4"></i>
<p>Chargement des playbooks...</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- END PAGE: PLAYBOOKS -->
<!-- ==================== PAGE: TASKS ==================== -->
<section id="page-tasks" class="page-section">
<div id="tasks" class="pt-24 pb-16 min-h-screen bg-black">
<div class="max-w-7xl mx-auto px-6">
<h2 class="text-3xl font-bold mb-12 text-center gradient-text fade-in">Gestion des Tâches</h2>
<div class="glass-card p-6 fade-in">
<!-- Header avec filtres -->
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between mb-6 gap-4">
<div class="flex items-center space-x-4">
<h3 class="text-xl font-semibold">Tâches</h3>
<span id="tasks-count-badge" class="px-3 py-1 bg-purple-600 text-sm rounded-full">0</span>
</div>
<!-- Boutons de filtrage par statut -->
<div class="flex flex-wrap gap-2" id="status-filters">
<button onclick="dashboard.filterTasksByStatus('all')" data-status="all"
class="task-filter-btn active px-4 py-2 rounded-lg text-sm font-medium transition-all bg-purple-600">
<i class="fas fa-list mr-2"></i>Toutes
<span class="ml-1 px-1.5 py-0.5 bg-white/20 rounded text-xs" id="count-all">0</span>
</button>
<button onclick="dashboard.filterTasksByStatus('running')" data-status="running"
class="task-filter-btn px-4 py-2 rounded-lg text-sm font-medium transition-all bg-gray-700 hover:bg-blue-600">
<i class="fas fa-spinner fa-spin mr-2"></i>En cours
<span class="ml-1 px-1.5 py-0.5 bg-white/20 rounded text-xs" id="count-running">0</span>
</button>
<button onclick="dashboard.filterTasksByStatus('completed')" data-status="completed"
class="task-filter-btn px-4 py-2 rounded-lg text-sm font-medium transition-all bg-gray-700 hover:bg-green-600">
<i class="fas fa-check-circle mr-2"></i>Terminées
<span class="ml-1 px-1.5 py-0.5 bg-white/20 rounded text-xs" id="count-completed">0</span>
</button>
<button onclick="dashboard.filterTasksByStatus('failed')" data-status="failed"
class="task-filter-btn px-4 py-2 rounded-lg text-sm font-medium transition-all bg-gray-700 hover:bg-red-600">
<i class="fas fa-times-circle mr-2"></i>Échouées
<span class="ml-1 px-1.5 py-0.5 bg-white/20 rounded text-xs" id="count-failed">0</span>
</button>
</div>
</div>
<!-- Filtrage par date (calendrier) -->
<div class="flex flex-wrap items-center gap-3 mb-6 p-4 bg-gray-800/50 rounded-lg">
<span class="text-sm text-gray-400 flex items-center">
<i class="fas fa-calendar-alt mr-2"></i>
Filtrer par date:
</span>
<!-- Bouton d'ouverture du calendrier -->
<div class="relative" id="task-date-filter-wrapper">
<button id="task-date-filter-button" type="button"
class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors bg-gray-900 text-gray-100 hover:bg-gray-800 h-10 px-4 py-2 min-w-[220px] border border-gray-700">
<i class="fas fa-calendar-day mr-2"></i>
<span id="task-date-filter-label">Toutes les dates</span>
</button>
<!-- Popup calendrier -->
<div id="task-date-calendar" class="hidden absolute top-full mt-2 left-0 z-20">
<div class="task-calendar shadow-2xl rounded-lg border border-gray-700 bg-gray-900 w-80 font-sans">
<!-- Header -->
<div class="flex items-center justify-between mb-3 px-3 pt-3">
<button type="button" id="task-cal-prev-month" class="p-1.5 rounded-full hover:bg-gray-800 text-gray-400 hover:text-gray-100 transition-colors">
<i class="fas fa-chevron-left text-xs"></i>
</button>
<p id="task-cal-current-month" class="text-sm font-semibold text-gray-100 text-center"></p>
<button type="button" id="task-cal-next-month" class="p-1.5 rounded-full hover:bg-gray-800 text-gray-400 hover:text-gray-100 transition-colors">
<i class="fas fa-chevron-right text-xs"></i>
</button>
</div>
<!-- Jours de la semaine -->
<div class="grid grid-cols-7 text-center text-[11px] text-gray-500 font-medium mb-1 px-2">
<div>Di</div>
<div>Lu</div>
<div>Ma</div>
<div>Me</div>
<div>Je</div>
<div>Ve</div>
<div>Sa</div>
</div>
<!-- Grille de dates -->
<div id="task-cal-grid" class="grid grid-cols-7 text-sm pb-2 px-1"></div>
<!-- Footer -->
<div class="flex justify-between items-center gap-2 mt-1 pt-2 border-t border-gray-800 px-3 pb-3">
<button id="task-cal-clear" type="button"
class="inline-flex items-center justify-center rounded-md text-xs font-medium transition-colors bg-gray-800 border border-gray-700 text-gray-200 hover:bg-gray-700 h-8 px-3 disabled:opacity-50 disabled:pointer-events-none">
Effacer
</button>
<div class="flex-1 text-right pr-2">
<span id="task-cal-summary" class="text-[11px] text-gray-400">Toutes les dates</span>
</div>
<button id="task-cal-apply" type="button"
class="inline-flex items-center justify-center rounded-md text-xs font-medium transition-colors bg-purple-600 text-white hover:bg-purple-500 h-8 px-3">
Appliquer
</button>
</div>
</div>
</div>
</div>
<button onclick="dashboard.clearDateFilters()" class="px-3 py-2 bg-gray-600 rounded-lg hover:bg-gray-500 text-sm">
<i class="fas fa-times mr-1"></i>Effacer
</button>
<button onclick="dashboard.refreshTaskLogs()" class="px-3 py-2 bg-purple-600 rounded-lg hover:bg-purple-500 text-sm ml-auto">
<i class="fas fa-sync-alt mr-1"></i>Rafraîchir
</button>
</div>
<!-- Liste des tâches -->
<div id="tasks-list" class="space-y-4">
<!-- Tasks will be populated by JavaScript -->
</div>
<!-- Pagination / Load more -->
<div id="tasks-pagination" class="mt-6 text-center hidden">
<button onclick="dashboard.loadMoreTasks()" class="px-6 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 text-sm">
<i class="fas fa-chevron-down mr-2"></i>Charger plus
</button>
</div>
</div>
</div>
</div>
</section>
<!-- END PAGE: TASKS -->
<!-- ==================== PAGE: LOGS ==================== -->
<section id="page-logs" class="page-section">
<div id="logs" class="pt-24 pb-16 min-h-screen bg-gradient-to-b from-black to-gray-900">
<div class="max-w-7xl mx-auto px-6">
<div class="text-center mb-12">
<h1 class="text-4xl font-bold mb-4 gradient-text">
<i class="fas fa-file-alt mr-3"></i>Logs Système
</h1>
<p class="text-gray-400">Consultez l'historique des opérations et événements système</p>
</div>
<div class="glass-card p-6 fade-in">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-semibold">Logs Récentes</h3>
<div class="flex space-x-3">
<button class="px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors" onclick="dashboard.clearLogs()">
<i class="fas fa-trash mr-2"></i>
Effacer
</button>
<button class="px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors" onclick="dashboard.exportLogs()">
<i class="fas fa-download mr-2"></i>
Exporter
</button>
</div>
</div>
<div id="logs-container" class="max-h-[600px] overflow-y-auto">
<!-- Logs will be populated by JavaScript -->
</div>
</div>
</div>
</div>
</section>
<!-- END PAGE: LOGS -->
<!-- ==================== PAGE: AIDE ==================== -->
<section id="page-help" class="page-section">
<div class="pt-24 pb-16 min-h-screen bg-gradient-to-b from-gray-900 to-black">
<div class="max-w-7xl mx-auto px-6">
<!-- Header -->
<div class="text-center mb-12">
<h1 class="text-4xl font-bold mb-4 gradient-text">
<i class="fas fa-question-circle mr-3"></i>Centre d'Aide
</h1>
<p class="text-gray-400 max-w-2xl mx-auto">
Bienvenue dans le guide d'utilisation du Homelab Automation Dashboard.
Découvrez comment gérer efficacement votre infrastructure.
</p>
</div>
<!-- Layout avec Table des Matières -->
<div class="flex gap-8">
<!-- Table des Matières (sidebar gauche) -->
<aside class="hidden lg:block w-64 flex-shrink-0">
<div class="help-toc glass-card p-4">
<div class="help-toc-title">Table des Matières</div>
<nav>
<a href="#help-quickstart" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-quickstart')">
<i class="fas fa-rocket mr-2 text-purple-400"></i>Démarrage Rapide
</a>
<a href="#help-indicators" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-indicators')">
<i class="fas fa-heartbeat mr-2 text-red-400"></i>Indicateurs de Santé
</a>
<a href="#help-architecture" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-architecture')">
<i class="fas fa-sitemap mr-2 text-blue-400"></i>Architecture
</a>
<a href="#help-features" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-features')">
<i class="fas fa-th-large mr-2 text-green-400"></i>Fonctionnalités
</a>
<a href="#help-playbooks" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-playbooks')">
<i class="fas fa-book mr-2 text-orange-400"></i>Playbooks Ansible
</a>
<a href="#help-api" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-api')">
<i class="fas fa-code mr-2 text-cyan-400"></i>Référence API
</a>
<a href="#help-troubleshooting" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-troubleshooting')">
<i class="fas fa-wrench mr-2 text-red-400"></i>Dépannage
</a>
<a href="#help-shortcuts" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-shortcuts')">
<i class="fas fa-keyboard mr-2 text-yellow-400"></i>Raccourcis
</a>
</nav>
</div>
</aside>
<!-- Contenu Principal -->
<div class="flex-1 help-main-content">
<!-- Quick Start -->
<div id="help-quickstart" class="glass-card p-8 mb-8 fade-in help-section-anchor">
<h2 class="help-section-title">
<i class="fas fa-rocket text-purple-500"></i>
Démarrage Rapide
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="help-card">
<div class="text-3xl mb-4 text-green-400"><i class="fas fa-1"></i></div>
<h3 class="font-semibold mb-2">Ajouter vos Hosts</h3>
<p class="text-gray-400 text-sm">
Commencez par ajouter vos serveurs dans la section <span class="help-code">Hosts</span>.
Chaque host nécessite un nom, une adresse IP et un système d'exploitation.
</p>
</div>
<div class="help-card">
<div class="text-3xl mb-4 text-blue-400"><i class="fas fa-2"></i></div>
<h3 class="font-semibold mb-2">Bootstrap Ansible</h3>
<p class="text-gray-400 text-sm">
Exécutez le <span class="help-code">Bootstrap</span> sur chaque host pour configurer
l'accès SSH et les prérequis Ansible.
</p>
</div>
<div class="help-card">
<div class="text-3xl mb-4 text-purple-400"><i class="fas fa-3"></i></div>
<h3 class="font-semibold mb-2">Automatiser</h3>
<p class="text-gray-400 text-sm">
Utilisez les <span class="help-code">Actions Rapides</span> ou exécutez des playbooks
personnalisés pour automatiser vos tâches.
</p>
</div>
</div>
</div>
<!-- Indicateurs de Santé -->
<div id="help-indicators" class="glass-card p-8 mb-8 fade-in help-section-anchor">
<h2 class="help-section-title">
<i class="fas fa-heartbeat text-red-500"></i>
Indicateurs de Santé des Hosts
</h2>
<p class="text-gray-400 mb-6">
Chaque host affiche un indicateur visuel de santé représenté par des barres colorées.
Cet indicateur combine plusieurs facteurs pour évaluer l'état global de votre serveur.
</p>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Explication des barres -->
<div>
<h3 class="font-semibold mb-4 text-purple-400">Comprendre l'Indicateur</h3>
<div class="space-y-4">
<!-- Niveau Excellent -->
<div class="flex items-center gap-4 p-3 bg-gray-800/50 rounded-lg">
<div class="health-indicator-demo">
<div class="health-bar bg-green-500"></div>
<div class="health-bar bg-green-500"></div>
<div class="health-bar bg-green-500"></div>
<div class="health-bar bg-green-500"></div>
<div class="health-bar bg-green-500"></div>
</div>
<div>
<span class="font-semibold text-green-400">Excellent</span>
<span class="text-gray-400 text-sm ml-2">(5 barres vertes)</span>
<p class="text-gray-500 text-xs mt-1">Host en ligne, bootstrap OK, vérifié récemment</p>
</div>
</div>
<!-- Niveau Bon -->
<div class="flex items-center gap-4 p-3 bg-gray-800/50 rounded-lg">
<div class="health-indicator-demo">
<div class="health-bar bg-yellow-500"></div>
<div class="health-bar bg-yellow-500"></div>
<div class="health-bar bg-yellow-500"></div>
<div class="health-bar bg-gray-600"></div>
<div class="health-bar bg-gray-600"></div>
</div>
<div>
<span class="font-semibold text-yellow-400">Bon</span>
<span class="text-gray-400 text-sm ml-2">(3-4 barres jaunes)</span>
<p class="text-gray-500 text-xs mt-1">Host fonctionnel mais certains aspects à améliorer</p>
</div>
</div>
<!-- Niveau Moyen -->
<div class="flex items-center gap-4 p-3 bg-gray-800/50 rounded-lg">
<div class="health-indicator-demo">
<div class="health-bar bg-orange-500"></div>
<div class="health-bar bg-orange-500"></div>
<div class="health-bar bg-gray-600"></div>
<div class="health-bar bg-gray-600"></div>
<div class="health-bar bg-gray-600"></div>
</div>
<div>
<span class="font-semibold text-orange-400">Moyen</span>
<span class="text-gray-400 text-sm ml-2">(2 barres oranges)</span>
<p class="text-gray-500 text-xs mt-1">Attention requise - vérification recommandée</p>
</div>
</div>
<!-- Niveau Faible -->
<div class="flex items-center gap-4 p-3 bg-gray-800/50 rounded-lg">
<div class="health-indicator-demo">
<div class="health-bar bg-red-500"></div>
<div class="health-bar bg-gray-600"></div>
<div class="health-bar bg-gray-600"></div>
<div class="health-bar bg-gray-600"></div>
<div class="health-bar bg-gray-600"></div>
</div>
<div>
<span class="font-semibold text-red-400">Faible</span>
<span class="text-gray-400 text-sm ml-2">(1 barre rouge)</span>
<p class="text-gray-500 text-xs mt-1">Host hors ligne ou non configuré</p>
</div>
</div>
</div>
</div>
<!-- Facteurs de calcul -->
<div>
<h3 class="font-semibold mb-4 text-purple-400">Facteurs de Calcul du Score</h3>
<div class="space-y-3">
<div class="p-3 bg-gray-800/50 rounded-lg">
<div class="flex items-center gap-2 mb-2">
<i class="fas fa-circle text-green-400 text-xs"></i>
<span class="font-medium">Statut en ligne</span>
<span class="text-green-400 text-sm ml-auto">+2 points</span>
</div>
<p class="text-gray-500 text-xs">Le host répond aux requêtes réseau</p>
</div>
<div class="p-3 bg-gray-800/50 rounded-lg">
<div class="flex items-center gap-2 mb-2">
<i class="fas fa-check-circle text-blue-400 text-xs"></i>
<span class="font-medium">Bootstrap Ansible OK</span>
<span class="text-blue-400 text-sm ml-auto">+1 point</span>
</div>
<p class="text-gray-500 text-xs">SSH et prérequis Ansible configurés</p>
</div>
<div class="p-3 bg-gray-800/50 rounded-lg">
<div class="flex items-center gap-2 mb-2">
<i class="fas fa-clock text-purple-400 text-xs"></i>
<span class="font-medium">Vérifié récemment (&lt;1h)</span>
<span class="text-purple-400 text-sm ml-auto">+2 points</span>
</div>
<p class="text-gray-500 text-xs">Dernière vérification il y a moins d'une heure</p>
</div>
<div class="p-3 bg-gray-800/50 rounded-lg">
<div class="flex items-center gap-2 mb-2">
<i class="fas fa-clock text-yellow-400 text-xs"></i>
<span class="font-medium">Vérifié aujourd'hui</span>
<span class="text-yellow-400 text-sm ml-auto">+1 point</span>
</div>
<p class="text-gray-500 text-xs">Dernière vérification dans les 24 dernières heures</p>
</div>
</div>
<div class="mt-4 p-3 bg-purple-900/20 border border-purple-600/30 rounded-lg">
<p class="text-sm text-purple-300">
<i class="fas fa-info-circle mr-2"></i>
<strong>Astuce:</strong> Exécutez régulièrement un <span class="help-code">Health Check</span>
pour maintenir un score de santé élevé.
</p>
</div>
</div>
</div>
<!-- Statuts Bootstrap -->
<div class="mt-8">
<h3 class="font-semibold mb-4 text-purple-400">Statuts Bootstrap Ansible</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="p-4 bg-green-900/20 border border-green-600/30 rounded-lg">
<div class="flex items-center gap-3 mb-2">
<span class="px-2 py-0.5 bg-green-600/30 text-green-400 text-xs rounded-full flex items-center">
<i class="fas fa-check-circle mr-1"></i>Ansible Ready
</span>
</div>
<p class="text-gray-400 text-sm">
Le host est entièrement configuré pour Ansible. L'utilisateur automation existe,
la clé SSH est déployée et sudo est configuré sans mot de passe.
</p>
</div>
<div class="p-4 bg-yellow-900/20 border border-yellow-600/30 rounded-lg">
<div class="flex items-center gap-3 mb-2">
<span class="px-2 py-0.5 bg-yellow-600/30 text-yellow-400 text-xs rounded-full flex items-center">
<i class="fas fa-exclamation-triangle mr-1"></i>Non configuré
</span>
</div>
<p class="text-gray-400 text-sm">
Le bootstrap n'a pas encore été exécuté sur ce host. Cliquez sur le bouton
<span class="help-code">Bootstrap</span> pour configurer l'accès Ansible.
</p>
</div>
</div>
</div>
<!-- Texte "Jamais vérifié" -->
<div class="mt-6 p-4 bg-gray-800/50 rounded-lg">
<h4 class="font-semibold mb-2 flex items-center gap-2">
<i class="fas fa-question-circle text-gray-400"></i>
Que signifie "Jamais vérifié" ?
</h4>
<p class="text-gray-400 text-sm">
Ce message apparaît lorsqu'aucun Health Check n'a été exécuté sur le host depuis son ajout.
Le système ne peut pas déterminer l'état réel du serveur. Lancez un Health Check pour
mettre à jour le statut et obtenir un score de santé précis.
</p>
</div>
</div>
<!-- Architecture -->
<div id="help-architecture" class="glass-card p-8 mb-8 fade-in help-section-anchor">
<h2 class="help-section-title">
<i class="fas fa-sitemap text-blue-500"></i>
Architecture de la Solution
</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div>
<h3 class="font-semibold mb-4 text-purple-400">Stack Technologique</h3>
<ul class="help-list">
<li class="flex items-center gap-3">
<i class="fas fa-server text-green-400"></i>
<span><strong>Backend:</strong> FastAPI (Python) - API REST haute performance</span>
</li>
<li class="flex items-center gap-3">
<i class="fas fa-cogs text-orange-400"></i>
<span><strong>Automation:</strong> Ansible - Gestion de configuration</span>
</li>
<li class="flex items-center gap-3">
<i class="fas fa-desktop text-blue-400"></i>
<span><strong>Frontend:</strong> HTML/CSS/JS avec TailwindCSS</span>
</li>
<li class="flex items-center gap-3">
<i class="fab fa-docker text-cyan-400"></i>
<span><strong>Déploiement:</strong> Docker & Docker Compose</span>
</li>
<li class="flex items-center gap-3">
<i class="fas fa-plug text-yellow-400"></i>
<span><strong>Temps réel:</strong> WebSocket pour les mises à jour live</span>
</li>
</ul>
</div>
<div>
<h3 class="font-semibold mb-4 text-purple-400">Structure des Fichiers</h3>
<div class="bg-black/40 p-4 rounded-lg font-mono text-sm">
<pre class="text-gray-300">
homelab-automation/
├── app/
│ ├── app_optimized.py # API FastAPI
│ ├── index.html # Interface web
│ └── main.js # Logique frontend
├── ansible/
│ ├── inventory/
│ │ ├── hosts.yml # Inventaire des hosts
│ │ └── group_vars/ # Variables par groupe
│ └── playbooks/ # Playbooks Ansible
├── tasks_logs/ # Logs des tâches
├── docker-compose.yml
└── Dockerfile</pre>
</div>
</div>
</div>
</div>
<!-- Fonctionnalités -->
<div id="help-features" class="glass-card p-8 mb-8 fade-in help-section-anchor">
<h2 class="help-section-title">
<i class="fas fa-th-large text-green-500"></i>
Fonctionnalités par Section
</h2>
<!-- Accordéon -->
<div class="space-y-2">
<!-- Dashboard -->
<div class="accordion-item" onclick="toggleAccordion(this)">
<div class="accordion-header">
<span class="flex items-center gap-3">
<i class="fas fa-tachometer-alt text-purple-400"></i>
<strong>Dashboard</strong>
</span>
<i class="fas fa-chevron-down accordion-icon text-gray-400"></i>
</div>
<div class="accordion-content">
<div class="p-4">
<p class="text-gray-400 mb-4">Vue d'ensemble de votre infrastructure avec métriques en temps réel.</p>
<ul class="help-list text-sm">
<li><strong>Métriques:</strong> Nombre d'hosts en ligne, tâches exécutées, taux de succès</li>
<li><strong>Actions Rapides:</strong> Mise à jour, redémarrage, health check, backup</li>
<li><strong>Aperçu Hosts:</strong> Liste condensée avec statut de chaque serveur</li>
</ul>
</div>
</div>
</div>
<!-- Hosts -->
<div class="accordion-item" onclick="toggleAccordion(this)">
<div class="accordion-header">
<span class="flex items-center gap-3">
<i class="fas fa-server text-blue-400"></i>
<strong>Hosts</strong>
</span>
<i class="fas fa-chevron-down accordion-icon text-gray-400"></i>
</div>
<div class="accordion-content">
<div class="p-4">
<p class="text-gray-400 mb-4">Gestion complète de vos serveurs et machines.</p>
<ul class="help-list text-sm">
<li><strong>Ajouter un Host:</strong> Nom, IP, OS, groupes d'environnement et de rôle</li>
<li><strong>Bootstrap:</strong> Configure SSH et les prérequis Ansible sur le host</li>
<li><strong>Filtres:</strong> Par groupe, par statut Ansible Ready/Non configuré</li>
<li><strong>Actions:</strong> Health Check, Upgrade, Reboot, Backup par host</li>
<li><strong>Playbooks:</strong> Exécuter des playbooks sur un groupe de hosts</li>
</ul>
</div>
</div>
</div>
<!-- Tasks -->
<div class="accordion-item" onclick="toggleAccordion(this)">
<div class="accordion-header">
<span class="flex items-center gap-3">
<i class="fas fa-tasks text-green-400"></i>
<strong>Tasks</strong>
</span>
<i class="fas fa-chevron-down accordion-icon text-gray-400"></i>
</div>
<div class="accordion-content">
<div class="p-4">
<p class="text-gray-400 mb-4">Historique et suivi des tâches d'automatisation.</p>
<ul class="help-list text-sm">
<li><strong>Statuts:</strong> En cours (bleu), Terminées (vert), Échouées (rouge)</li>
<li><strong>Filtres:</strong> Par statut, par date (année/mois/jour)</li>
<li><strong>Détails:</strong> Cliquez sur une tâche pour voir les logs complets</li>
<li><strong>Polling:</strong> Mise à jour automatique des tâches en cours</li>
</ul>
</div>
</div>
</div>
<!-- Logs -->
<div class="accordion-item" onclick="toggleAccordion(this)">
<div class="accordion-header">
<span class="flex items-center gap-3">
<i class="fas fa-file-alt text-orange-400"></i>
<strong>Logs</strong>
</span>
<i class="fas fa-chevron-down accordion-icon text-gray-400"></i>
</div>
<div class="accordion-content">
<div class="p-4">
<p class="text-gray-400 mb-4">Journal des événements système en temps réel.</p>
<ul class="help-list text-sm">
<li><strong>Types:</strong> Info, Warning, Error avec codes couleur</li>
<li><strong>WebSocket:</strong> Logs en temps réel sans rafraîchissement</li>
<li><strong>Export:</strong> Téléchargez les logs au format texte</li>
<li><strong>Effacer:</strong> Nettoyez l'historique des logs</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Playbooks Ansible -->
<div id="help-playbooks" class="glass-card p-8 mb-8 fade-in help-section-anchor">
<h2 class="help-section-title">
<i class="fas fa-book text-orange-500"></i>
Playbooks Ansible Disponibles
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="help-card">
<h3 class="font-semibold mb-3 flex items-center gap-2">
<i class="fas fa-tools text-yellow-400"></i>
bootstrap-host.yml
</h3>
<p class="text-gray-400 text-sm mb-2">
Configure un nouveau host pour Ansible: création utilisateur, clé SSH, sudo sans mot de passe.
</p>
<span class="text-xs text-purple-400">Requis avant toute autre opération</span>
</div>
<div class="help-card">
<h3 class="font-semibold mb-3 flex items-center gap-2">
<i class="fas fa-heartbeat text-green-400"></i>
health-check.yml
</h3>
<p class="text-gray-400 text-sm mb-2">
Vérifie l'état de santé: CPU, RAM, disque, services critiques.
</p>
<span class="text-xs text-purple-400">Exécution rapide, non destructif</span>
</div>
<div class="help-card">
<h3 class="font-semibold mb-3 flex items-center gap-2">
<i class="fas fa-arrow-up text-blue-400"></i>
system-upgrade.yml
</h3>
<p class="text-gray-400 text-sm mb-2">
Met à jour tous les paquets système (apt/yum/dnf selon l'OS).
</p>
<span class="text-xs text-orange-400">Peut nécessiter un redémarrage</span>
</div>
<div class="help-card">
<h3 class="font-semibold mb-3 flex items-center gap-2">
<i class="fas fa-save text-cyan-400"></i>
backup-config.yml
</h3>
<p class="text-gray-400 text-sm mb-2">
Sauvegarde les fichiers de configuration importants (/etc, configs apps).
</p>
<span class="text-xs text-purple-400">Stockage local ou distant</span>
</div>
</div>
</div>
<!-- API Reference -->
<div id="help-api" class="glass-card p-8 mb-8 fade-in help-section-anchor">
<h2 class="help-section-title">
<i class="fas fa-code text-cyan-500"></i>
Référence API
</h2>
<p class="text-gray-400 mb-6">
L'API REST est accessible sur le port configuré. Authentification via header <span class="help-code">X-API-Key</span>.
</p>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-700">
<th class="text-left py-3 px-4 text-purple-400">Endpoint</th>
<th class="text-left py-3 px-4 text-purple-400">Méthode</th>
<th class="text-left py-3 px-4 text-purple-400">Description</th>
</tr>
</thead>
<tbody class="text-gray-300">
<tr class="border-b border-gray-800">
<td class="py-3 px-4"><span class="help-code">/api/hosts</span></td>
<td class="py-3 px-4"><span class="text-green-400">GET</span></td>
<td class="py-3 px-4">Liste tous les hosts</td>
</tr>
<tr class="border-b border-gray-800">
<td class="py-3 px-4"><span class="help-code">/api/hosts</span></td>
<td class="py-3 px-4"><span class="text-blue-400">POST</span></td>
<td class="py-3 px-4">Ajoute un nouveau host</td>
</tr>
<tr class="border-b border-gray-800">
<td class="py-3 px-4"><span class="help-code">/api/tasks/logs</span></td>
<td class="py-3 px-4"><span class="text-green-400">GET</span></td>
<td class="py-3 px-4">Récupère les logs de tâches</td>
</tr>
<tr class="border-b border-gray-800">
<td class="py-3 px-4"><span class="help-code">/api/ansible/playbooks</span></td>
<td class="py-3 px-4"><span class="text-green-400">GET</span></td>
<td class="py-3 px-4">Liste les playbooks disponibles</td>
</tr>
<tr class="border-b border-gray-800">
<td class="py-3 px-4"><span class="help-code">/api/ansible/execute</span></td>
<td class="py-3 px-4"><span class="text-blue-400">POST</span></td>
<td class="py-3 px-4">Exécute un playbook</td>
</tr>
<tr>
<td class="py-3 px-4"><span class="help-code">/api/metrics</span></td>
<td class="py-3 px-4"><span class="text-green-400">GET</span></td>
<td class="py-3 px-4">Métriques du dashboard</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Troubleshooting -->
<div id="help-troubleshooting" class="glass-card p-8 mb-8 fade-in help-section-anchor">
<h2 class="help-section-title">
<i class="fas fa-wrench text-red-500"></i>
Dépannage
</h2>
<div class="space-y-4">
<div class="accordion-item" onclick="toggleAccordion(this)">
<div class="accordion-header">
<span>Le bootstrap échoue avec "Permission denied"</span>
<i class="fas fa-chevron-down accordion-icon text-gray-400"></i>
</div>
<div class="accordion-content">
<div class="p-4 text-gray-400 text-sm">
<p class="mb-2"><strong>Cause:</strong> Les identifiants SSH fournis sont incorrects ou l'utilisateur n'a pas les droits sudo.</p>
<p><strong>Solution:</strong> Vérifiez le nom d'utilisateur et mot de passe. Assurez-vous que l'utilisateur peut exécuter <span class="help-code">sudo</span> sur le host cible.</p>
</div>
</div>
</div>
<div class="accordion-item" onclick="toggleAccordion(this)">
<div class="accordion-header">
<span>Les hosts apparaissent "offline" alors qu'ils sont accessibles</span>
<i class="fas fa-chevron-down accordion-icon text-gray-400"></i>
</div>
<div class="accordion-content">
<div class="p-4 text-gray-400 text-sm">
<p class="mb-2"><strong>Cause:</strong> Le health check n'a pas été exécuté ou la clé SSH n'est pas configurée.</p>
<p><strong>Solution:</strong> Exécutez le bootstrap si ce n'est pas fait, puis lancez un Health Check.</p>
</div>
</div>
</div>
<div class="accordion-item" onclick="toggleAccordion(this)">
<div class="accordion-header">
<span>Les tâches restent bloquées "En cours"</span>
<i class="fas fa-chevron-down accordion-icon text-gray-400"></i>
</div>
<div class="accordion-content">
<div class="p-4 text-gray-400 text-sm">
<p class="mb-2"><strong>Cause:</strong> Le processus Ansible peut être bloqué ou le host ne répond plus.</p>
<p><strong>Solution:</strong> Vérifiez la connectivité réseau. Consultez les logs système pour plus de détails. Redémarrez le conteneur Docker si nécessaire.</p>
</div>
</div>
</div>
<div class="accordion-item" onclick="toggleAccordion(this)">
<div class="accordion-header">
<span>L'interface ne se met pas à jour en temps réel</span>
<i class="fas fa-chevron-down accordion-icon text-gray-400"></i>
</div>
<div class="accordion-content">
<div class="p-4 text-gray-400 text-sm">
<p class="mb-2"><strong>Cause:</strong> La connexion WebSocket est interrompue.</p>
<p><strong>Solution:</strong> Rafraîchissez la page. Vérifiez que le port WebSocket n'est pas bloqué par un firewall ou proxy.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Keyboard Shortcuts -->
<div id="help-shortcuts" class="glass-card p-8 fade-in help-section-anchor">
<h2 class="help-section-title">
<i class="fas fa-keyboard text-yellow-500"></i>
Raccourcis & Astuces
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 class="font-semibold mb-4 text-purple-400">Navigation</h3>
<ul class="help-list text-sm">
<li>Cliquez sur le logo pour revenir au Dashboard</li>
<li>Utilisez les onglets du menu pour naviguer</li>
<li>Le thème clair/sombre est persistant</li>
</ul>
</div>
<div>
<h3 class="font-semibold mb-4 text-purple-400">Productivité</h3>
<ul class="help-list text-sm">
<li>Filtrez les hosts par groupe pour des actions groupées</li>
<li>Utilisez les filtres de date pour retrouver des tâches</li>
<li>Exportez les logs avant de les effacer</li>
</ul>
</div>
</div>
</div>
</div><!-- Fin help-main-content -->
</div><!-- Fin flex container -->
</div>
</div>
</section>
<!-- END PAGE: AIDE -->
<!-- Footer -->
<footer class="bg-black border-t border-gray-800 py-8">
<div class="max-w-7xl mx-auto px-6 text-center">
<p class="text-gray-400">
© 2025 Homelab Automation Dashboard. Propulsé par
<span class="gradient-text">FastAPI</span>,
<span class="gradient-text">Ansible</span> et
<span class="gradient-text">Technologies Modernes</span>
</p>
</div>
</footer>
<!-- Loading Overlay -->
<div id="loading-overlay" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 hidden">
<div class="text-center">
<div class="loading-spinner mx-auto mb-4"></div>
<p class="text-white text-lg">Exécution de la tâche...</p>
</div>
</div>
<!-- Modal -->
<div id="modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden overflow-y-auto py-8">
<div class="glass-card p-8 max-w-4xl w-full mx-4 my-auto">
<div class="flex items-center justify-between mb-6">
<h3 id="modal-title" class="text-xl font-semibold">Titre du Modal</h3>
<button onclick="dashboard.closeModal()" class="text-gray-400 hover:text-white">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div id="modal-content">
<!-- Modal content will be populated by JavaScript -->
</div>
</div>
</div>
<script src="/static/main.js"></script>
<!-- Navigation & Help Page Scripts -->
<script>
// ===== PAGE NAVIGATION =====
let currentPage = 'dashboard';
function navigateTo(pageName) {
// Hide all pages
document.querySelectorAll('.page-section').forEach(page => {
page.classList.remove('active');
});
// Show target page
const targetPage = document.getElementById(`page-${pageName}`);
if (targetPage) {
targetPage.classList.add('active');
currentPage = pageName;
// Update nav links
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
if (link.dataset.page === pageName) {
link.classList.add('active');
}
});
// Update URL hash
window.location.hash = pageName;
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
// Trigger fade-in animations
targetPage.querySelectorAll('.fade-in').forEach(el => {
el.classList.add('visible');
});
}
}
// Initialize navigation
document.addEventListener('DOMContentLoaded', function() {
// Setup nav link clicks
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
navigateTo(this.dataset.page);
});
});
// Logo click returns to dashboard
const logo = document.querySelector('nav .flex.items-center.space-x-3');
if (logo) {
logo.style.cursor = 'pointer';
logo.addEventListener('click', function() {
navigateTo('dashboard');
});
}
// Handle initial hash
const hash = window.location.hash.replace('#', '');
if (hash && document.getElementById(`page-${hash}`)) {
navigateTo(hash);
} else {
// Set dashboard as active by default
document.querySelector('.nav-link[data-page="dashboard"]')?.classList.add('active');
}
// Handle browser back/forward
window.addEventListener('hashchange', function() {
const hash = window.location.hash.replace('#', '');
if (hash && hash !== currentPage) {
navigateTo(hash);
}
});
});
// ===== ACCORDION FOR HELP PAGE =====
function toggleAccordion(item) {
// Close other accordions in the same container
const container = item.parentElement;
container.querySelectorAll('.accordion-item').forEach(acc => {
if (acc !== item) {
acc.classList.remove('open');
}
});
// Toggle current accordion
item.classList.toggle('open');
}
// ===== TABLE DES MATIERES NAVIGATION =====
function scrollToHelpSection(event, sectionId) {
event.preventDefault();
const section = document.getElementById(sectionId);
if (section) {
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
// Mettre à jour l'élément actif dans la TOC
document.querySelectorAll('.help-toc-item').forEach(item => {
item.classList.remove('active');
});
event.currentTarget.classList.add('active');
}
}
// Observer pour mettre à jour la TOC lors du scroll
function initHelpTocObserver() {
const sections = document.querySelectorAll('.help-section-anchor');
const tocItems = document.querySelectorAll('.help-toc-item');
if (sections.length === 0 || tocItems.length === 0) return;
const observerOptions = {
root: null,
rootMargin: '-100px 0px -50% 0px',
threshold: 0
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const sectionId = entry.target.id;
tocItems.forEach(item => {
item.classList.remove('active');
if (item.getAttribute('href') === '#' + sectionId) {
item.classList.add('active');
}
});
}
});
}, observerOptions);
sections.forEach(section => observer.observe(section));
}
// Initialiser l'observer quand on navigue vers la page d'aide
document.addEventListener('DOMContentLoaded', function() {
// Observer pour détecter quand la page d'aide devient visible
const helpPage = document.getElementById('page-help');
if (helpPage) {
const pageObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.target.classList.contains('active')) {
setTimeout(initHelpTocObserver, 100);
}
});
});
pageObserver.observe(helpPage, { attributes: true, attributeFilter: ['class'] });
}
});
</script>
</body>
</html>