homelab_automation/app/index.html
Bruno Charest 05087aa380
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Replace manual upsert logic with SQLite native upsert in Docker CRUD repositories, enhance Ansible backup playbook with better error handling and file permissions, add favicon endpoint, and improve playbook editor UI with syntax highlighting, lint integration, quality badges, and enhanced code editing features
2025-12-17 15:36:49 -05:00

5358 lines
222 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);
}
.metric-card-clickable {
cursor: pointer;
position: relative;
}
.metric-card-clickable:hover {
border-color: rgba(16, 185, 129, 0.5);
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.15);
}
.metric-card-clickable:active {
transform: translateY(0);
}
.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);
}
/* Drawer tabs styling */
.drawer-tab {
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.2s ease;
}
.drawer-tab:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.drawer-tab.active {
border-bottom-color: var(--accent-color);
color: var(--accent-color);
}
/* Docker tabs styling */
.docker-tab {
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.2s ease;
}
.docker-tab:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.docker-tab.active {
border-bottom-color: var(--accent-color);
color: var(--accent-color);
}
/* 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: #4b5563 !important;
}
body.light-theme .text-gray-400 {
color: #374151 !important;
}
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-gray-800\/40 {
background-color: rgba(229, 231, 235, 0.75) !important;
}
body.light-theme .bg-gray-800\/60 {
background-color: rgba(229, 231, 235, 0.9) !important;
}
body.light-theme .border-gray-700\/60 {
border-color: #d1d5db !important;
}
body.light-theme .border-red-700\/60 {
border-color: #fca5a5 !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;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Navigation active state */
.desktop-nav-links .nav-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 10px;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
.desktop-nav-links .nav-link:hover {
background: rgba(255, 255, 255, 0.06);
}
.desktop-nav-links .nav-link.active {
color: #c4b5fd !important;
background: rgba(124, 58, 237, 0.18);
box-shadow: 0 0 0 1px rgba(124, 58, 237, 0.25) inset;
}
body.light-theme .desktop-nav-links .nav-link:hover {
background: rgba(0, 0, 0, 0.06);
}
body.light-theme .desktop-nav-links .nav-link.active {
color: #6d28d9 !important;
background: rgba(124, 58, 237, 0.14);
box-shadow: 0 0 0 1px rgba(124, 58, 237, 0.22) inset;
}
/* 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%);
}
body.light-theme #page-hosts > div,
body.light-theme #page-playbooks > div,
body.light-theme #page-schedules > div,
body.light-theme #page-alerts > div,
body.light-theme #page-docker > div,
body.light-theme #page-configuration > div {
background: linear-gradient(180deg, #f9fafb 0%, #e5e7eb 100%) !important;
}
/* 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;
}
.playbook-lint-badge {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
padding: 3px 8px;
border-radius: 9999px;
font-weight: 700;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(75, 85, 99, 0.35);
color: #e5e7eb;
line-height: 1;
cursor: help;
}
.playbook-lint-badge.ok {
background: rgba(59, 130, 246, 0.18);
color: #93c5fd;
border-color: rgba(59, 130, 246, 0.35);
}
.playbook-lint-badge.good {
background: rgba(34, 197, 94, 0.18);
color: #86efac;
border-color: rgba(34, 197, 94, 0.35);
}
.playbook-lint-badge.warn {
background: rgba(245, 158, 11, 0.18);
color: #fcd34d;
border-color: rgba(245, 158, 11, 0.35);
}
.playbook-lint-badge.poor {
background: rgba(239, 68, 68, 0.18);
color: #fca5a5;
border-color: rgba(239, 68, 68, 0.35);
}
/* 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;
}
/* Enhanced code textarea editor */
.playbook-code-editor {
width: 100%;
height: 100%;
min-height: 400px;
background: linear-gradient(to right, #252526 50px, #1e1e1e 50px);
color: #d4d4d4;
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
font-size: 14px;
line-height: 1.6;
padding: 16px 16px 16px 60px;
border: none;
resize: none;
outline: none;
tab-size: 2;
-moz-tab-size: 2;
white-space: pre;
overflow-wrap: normal;
overflow-x: auto;
caret-color: #a78bfa;
}
.playbook-code-editor:focus {
outline: none;
box-shadow: inset 0 0 0 2px rgba(139, 92, 246, 0.3);
}
.playbook-code-editor::selection {
background: rgba(139, 92, 246, 0.4);
}
.playbook-code-editor::-moz-selection {
background: rgba(139, 92, 246, 0.4);
}
/* Scrollbar styling for editor */
.playbook-code-editor::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.playbook-code-editor::-webkit-scrollbar-track {
background: #1e1e1e;
}
.playbook-code-editor::-webkit-scrollbar-thumb {
background: #424242;
border-radius: 5px;
}
.playbook-code-editor::-webkit-scrollbar-thumb:hover {
background: #555;
}
.playbook-code-editor::-webkit-scrollbar-corner {
background: #1e1e1e;
}
/* 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;
}
/* ===== ENHANCED PLAYBOOK EDITOR STYLES ===== */
/* Lint button states */
.lint-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s ease;
background: rgba(107, 114, 128, 0.3);
color: #d1d5db;
border: 1px solid rgba(107, 114, 128, 0.5);
}
.lint-button:hover:not(:disabled) {
background: rgba(139, 92, 246, 0.3);
border-color: rgba(139, 92, 246, 0.5);
}
.lint-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.lint-button.success {
background: rgba(16, 185, 129, 0.2);
border-color: rgba(16, 185, 129, 0.5);
color: #10b981;
}
.lint-button.warnings {
background: rgba(245, 158, 11, 0.2);
border-color: rgba(245, 158, 11, 0.5);
color: #f59e0b;
}
.lint-button.errors {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.5);
color: #ef4444;
}
/* Quality badge */
.quality-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.quality-badge.excellent {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.quality-badge.good {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.quality-badge.warning {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.quality-badge.poor {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
/* Problems panel */
.problems-panel {
max-height: 200px;
overflow-y: auto;
background: rgba(17, 24, 39, 0.8);
border: 1px solid #374151;
border-radius: 8px;
scrollbar-width: thin;
scrollbar-color: rgba(139, 92, 246, 0.5) rgba(0, 0, 0, 0.3);
}
.problems-panel::-webkit-scrollbar {
width: 6px;
}
.problems-panel::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
}
.problems-panel::-webkit-scrollbar-thumb {
background: rgba(139, 92, 246, 0.5);
border-radius: 3px;
}
.problem-item {
cursor: pointer;
transition: background 0.15s ease;
}
.problem-item:hover {
background: rgba(139, 92, 246, 0.1);
}
/* Lint gutter markers */
.cm-lint-gutter {
width: 20px;
}
.lint-gutter-marker {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
font-size: 12px;
cursor: pointer;
}
.lint-gutter-marker.lint-error {
color: #ef4444;
}
.lint-gutter-marker.lint-warning {
color: #f59e0b;
}
.lint-gutter-marker.lint-info {
color: #3b82f6;
}
/* CodeMirror 6 custom styles */
.cm-editor {
height: 100%;
font-size: 14px;
}
.cm-editor .cm-scroller {
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
line-height: 1.6;
}
.cm-editor .cm-gutters {
background: #1e1e1e;
border-right: 1px solid #333;
}
.cm-editor .cm-activeLineGutter {
background: rgba(139, 92, 246, 0.1);
}
.cm-editor .cm-activeLine {
background: rgba(139, 92, 246, 0.05);
}
/* Ansible syntax highlighting */
.cm-ansible-module {
color: #82aaff;
font-weight: 500;
}
.cm-ansible-key {
color: #c792ea;
}
.cm-jinja-var {
color: #c3e88d;
background: rgba(195, 232, 141, 0.1);
border-radius: 2px;
padding: 0 2px;
}
.cm-jinja-block {
color: #89ddff;
background: rgba(137, 221, 255, 0.1);
border-radius: 2px;
padding: 0 2px;
}
/* Editor tabs */
.editor-tabs {
display: flex;
gap: 2px;
background: rgba(17, 24, 39, 0.5);
padding: 4px;
border-radius: 8px;
}
.editor-tab {
padding: 6px 12px;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 500;
color: #9ca3af;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
gap: 6px;
}
.editor-tab:hover {
background: rgba(107, 114, 128, 0.3);
color: #e5e7eb;
}
.editor-tab.active {
background: rgba(139, 92, 246, 0.3);
color: #a78bfa;
}
.editor-tab .tab-count {
padding: 1px 6px;
border-radius: 9999px;
font-size: 0.65rem;
background: rgba(0, 0, 0, 0.3);
}
.editor-tab.errors .tab-count {
background: rgba(239, 68, 68, 0.3);
color: #fca5a5;
}
.editor-tab.warnings .tab-count {
background: rgba(245, 158, 11, 0.3);
color: #fcd34d;
}
/* 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;
}
/* ===== SCHEDULER PAGE STYLES ===== */
.schedule-card {
background: rgba(42, 42, 42, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 16px 20px;
transition: all 0.2s ease;
}
.schedule-card:hover {
background: rgba(42, 42, 42, 0.7);
border-color: rgba(124, 58, 237, 0.3);
transform: translateX(4px);
}
.schedule-card.paused {
opacity: 0.7;
border-left: 3px solid #f59e0b;
}
.schedule-card.active {
border-left: 3px solid #10b981;
}
.schedule-status-chip {
font-size: 0.7rem;
padding: 2px 8px;
border-radius: 9999px;
font-weight: 500;
}
.schedule-status-chip.active {
background-color: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.schedule-status-chip.paused {
background-color: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.schedule-status-chip.running {
background-color: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.schedule-status-chip.success {
background-color: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.schedule-status-chip.failed {
background-color: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.schedule-status-chip.scheduled {
background-color: rgba(124, 58, 237, 0.2);
color: #a78bfa;
}
.schedule-tag {
font-size: 0.65rem;
padding: 2px 6px;
border-radius: 4px;
background-color: rgba(107, 114, 128, 0.3);
color: #9ca3af;
}
.schedule-action-btn {
padding: 6px 10px;
border-radius: 6px;
font-size: 0.75rem;
transition: all 0.15s ease;
}
.schedule-action-btn:hover {
transform: scale(1.05);
}
.schedule-action-btn.run {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.schedule-action-btn.run:hover {
background: rgba(16, 185, 129, 0.4);
}
.schedule-action-btn.pause {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.schedule-action-btn.pause:hover {
background: rgba(245, 158, 11, 0.4);
}
.schedule-action-btn.edit {
background: rgba(59, 130, 246, 0.2);
color: #60a5fa;
}
.schedule-action-btn.edit:hover {
background: rgba(59, 130, 246, 0.4);
}
.schedule-action-btn.delete {
background: rgba(239, 68, 68, 0.2);
color: #f87171;
}
.schedule-action-btn.delete:hover {
background: rgba(239, 68, 68, 0.4);
}
.schedule-filter-btn.active {
background-color: #7c3aed !important;
color: white !important;
}
/* Calendar styles */
.schedule-calendar-day {
min-height: 80px;
background: rgba(42, 42, 42, 0.3);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 8px;
transition: all 0.2s ease;
}
.schedule-calendar-day:hover {
background: rgba(42, 42, 42, 0.6);
border-color: rgba(124, 58, 237, 0.3);
}
.schedule-calendar-day.today {
border-color: #7c3aed;
background: rgba(124, 58, 237, 0.1);
}
.schedule-calendar-day.other-month {
opacity: 0.4;
}
.schedule-calendar-event {
font-size: 0.65rem;
padding: 2px 4px;
border-radius: 4px;
margin-top: 2px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.schedule-calendar-event.success {
background-color: rgba(16, 185, 129, 0.3);
color: #10b981;
}
.schedule-calendar-event.failed {
background-color: rgba(239, 68, 68, 0.3);
color: #ef4444;
}
.schedule-calendar-event.scheduled {
background-color: rgba(59, 130, 246, 0.3);
color: #60a5fa;
}
/* Modal multi-step */
.schedule-modal-step {
display: none;
}
.schedule-modal-step.active {
display: block;
animation: fadeIn 0.3s ease;
}
.schedule-step-indicator {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 24px;
}
.schedule-step-dot {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: 600;
background: rgba(107, 114, 128, 0.3);
color: #9ca3af;
transition: all 0.3s ease;
}
.schedule-step-dot.active {
background: #7c3aed;
color: white;
}
.schedule-step-dot.completed {
background: #10b981;
color: white;
}
.schedule-step-connector {
width: 40px;
height: 2px;
background: rgba(107, 114, 128, 0.3);
align-self: center;
}
.schedule-step-connector.active {
background: #7c3aed;
}
/* Recurrence preview */
.recurrence-preview {
background: rgba(124, 58, 237, 0.1);
border: 1px solid rgba(124, 58, 237, 0.3);
border-radius: 8px;
padding: 12px 16px;
font-size: 0.875rem;
}
/* Light theme overrides */
body.light-theme .schedule-card {
background: rgba(255, 255, 255, 0.6);
border-color: #d1d5db;
}
body.light-theme .schedule-card:hover {
background: rgba(255, 255, 255, 0.9);
}
body.light-theme .schedule-calendar-day {
background: rgba(255, 255, 255, 0.6);
border-color: #e5e7eb;
}
body.light-theme .schedule-calendar-day:hover {
background: rgba(255, 255, 255, 0.9);
}
/* ========================================
MOBILE-FIRST RESPONSIVE STYLES
======================================== */
/* === MOBILE NAVIGATION === */
.mobile-menu-btn {
display: none;
width: 44px;
height: 44px;
align-items: center;
justify-content: center;
border-radius: 8px;
background: rgba(55, 65, 81, 0.5);
border: none;
cursor: pointer;
transition: all 0.2s ease;
-webkit-tap-highlight-color: transparent;
}
.mobile-menu-btn:hover,
.mobile-menu-btn:active {
background: rgba(124, 58, 237, 0.3);
}
.mobile-menu-btn .hamburger-icon {
width: 20px;
height: 14px;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.mobile-menu-btn .hamburger-icon span {
display: block;
height: 2px;
background: white;
border-radius: 2px;
transition: all 0.3s ease;
}
.mobile-menu-btn.active .hamburger-icon span:nth-child(1) {
transform: rotate(45deg) translate(4px, 4px);
}
.mobile-menu-btn.active .hamburger-icon span:nth-child(2) {
opacity: 0;
}
.mobile-menu-btn.active .hamburger-icon span:nth-child(3) {
transform: rotate(-45deg) translate(4px, -4px);
}
/* Mobile Menu Overlay */
.mobile-nav-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
z-index: 45;
opacity: 0;
transition: opacity 0.3s ease;
}
.mobile-nav-overlay.active {
opacity: 1;
}
/* Mobile Sidebar Navigation */
.mobile-nav-sidebar {
display: none;
position: fixed;
top: 0;
left: 0;
width: 280px;
max-width: 85vw;
height: 100vh;
background: var(--secondary-bg);
border-right: 1px solid var(--border-color);
z-index: 55;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.mobile-nav-sidebar.active {
transform: translateX(0);
}
.mobile-nav-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
}
.mobile-nav-links {
padding: 16px 0;
}
.mobile-nav-links button,
.mobile-nav-links a {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 24px;
color: var(--secondary-text);
text-decoration: none;
transition: all 0.2s ease;
min-height: 48px;
width: 100%;
background: none;
border: none;
border-left: 3px solid transparent;
font-size: 1rem;
cursor: pointer;
text-align: left;
}
.mobile-nav-links button:hover,
.mobile-nav-links button:active,
.mobile-nav-links a:hover,
.mobile-nav-links a:active {
background: rgba(124, 58, 237, 0.1);
color: var(--primary-text);
}
.mobile-nav-links button.active,
.mobile-nav-links a.active {
background: rgba(124, 58, 237, 0.15);
color: var(--accent-color);
border-left: 3px solid var(--accent-color);
}
.mobile-nav-links button i,
.mobile-nav-links a i {
width: 24px;
text-align: center;
}
/* ===== MOBILE DROPDOWN FIX ===== */
/* Force visibility when dropdown-open class is applied (overrides Tailwind) */
.group.dropdown-open > .absolute,
.group.dropdown-open > div.absolute,
.group.dropdown-open > div[class*="absolute"] {
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
display: block !important;
}
/* On touch devices, disable hover-based dropdowns */
@media (hover: none), (pointer: coarse) {
/* Hide dropdown by default on touch */
.group > .absolute,
.group > div.absolute {
visibility: hidden !important;
opacity: 0 !important;
}
/* But show when dropdown-open is applied */
.group.dropdown-open > .absolute,
.group.dropdown-open > div.absolute {
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
}
}
/* On small screens, always use click-based dropdowns */
@media (max-width: 768px) {
.group > .absolute,
.group > div.absolute {
visibility: hidden !important;
opacity: 0 !important;
}
.group.dropdown-open > .absolute,
.group.dropdown-open > div.absolute {
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
}
}
/* Touch feedback - subtle opacity change only */
.touch-active {
opacity: 0.8;
}
/* ===== ENSURE ALL INTERACTIVE ELEMENTS ARE CLICKABLE ===== */
button, .btn-primary, select, input, a, [onclick], [role="button"] {
cursor: pointer;
-webkit-tap-highlight-color: rgba(124, 58, 237, 0.3);
/* Ensure element is clickable */
position: relative;
z-index: 1;
}
/* Touch optimization for all interactive elements */
button, select, input, a, .touch-target, .mobile-menu-btn, .mobile-nav-link, [onclick] {
touch-action: manipulation;
-webkit-touch-callout: none;
user-select: none;
}
/* Prevent any pseudo-elements from blocking clicks */
button::before, button::after,
.btn-primary::before, .btn-primary::after {
pointer-events: none;
}
/* Desktop Navigation Links */
.desktop-nav-links {
display: flex;
align-items: center;
gap: 1.5rem;
}
/* === TOUCH TARGETS - Min 44x44px === */
.touch-target {
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* === MOBILE SELECT & BUTTON STYLING === */
@media (max-width: 768px) {
/* Ensure select dropdowns are touch-friendly */
select {
min-height: 44px;
font-size: 16px; /* Prevents iOS zoom on focus */
-webkit-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%239ca3af'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
padding-right: 32px;
}
/* Touch-friendly buttons */
button {
min-height: 40px;
}
/* Prevent double-tap zoom on buttons */
button, select, input, a {
touch-action: manipulation;
}
}
/* === MOBILE CARDS === */
.mobile-card-stack {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Table to Cards Transformation */
.responsive-table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* === MOBILE MODALS === */
@media (max-width: 640px) {
#modal .glass-card {
margin: 0;
border-radius: 0;
min-height: 100vh;
max-height: 100vh;
width: 100%;
max-width: 100%;
}
#modal {
padding: 0;
}
/* Modal close button fixed */
.modal-close-fixed {
position: fixed;
top: 12px;
right: 12px;
z-index: 60;
width: 44px;
height: 44px;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
}
/* === MOBILE HERO === */
@media (max-width: 640px) {
.hero-section h1 {
font-size: 2rem;
line-height: 1.2;
}
.hero-section p {
font-size: 1rem;
}
.animate-float {
animation: none;
}
}
/* === MOBILE METRIC CARDS === */
@media (max-width: 640px) {
.metric-card {
padding: 16px;
}
.metric-card .text-3xl {
font-size: 1.5rem;
}
}
/* === MOBILE BUTTONS === */
@media (max-width: 640px) {
.btn-primary {
padding: 14px 20px;
width: 100%;
justify-content: center;
}
/* Button group stacking */
.button-group-mobile {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.button-group-mobile > button,
.button-group-mobile > a {
width: 100%;
}
}
/* === MOBILE FILTERS === */
@media (max-width: 768px) {
.filter-bar-mobile {
display: flex;
flex-direction: column;
gap: 12px;
}
.filter-bar-mobile .flex-wrap {
flex-wrap: nowrap;
overflow-x: auto;
padding-bottom: 8px;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.filter-bar-mobile .flex-wrap::-webkit-scrollbar {
display: none;
}
/* Filter buttons as pills */
.task-filter-btn,
.schedule-filter-btn,
.playbook-filter-btn {
flex-shrink: 0;
white-space: nowrap;
padding: 10px 16px;
min-height: 44px;
}
}
/* === MOBILE HOST CARDS === */
@media (max-width: 768px) {
.host-card {
padding: 16px;
}
.host-card-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.host-card-actions button {
flex: 1;
min-width: calc(50% - 4px);
min-height: 44px;
font-size: 0.75rem;
}
/* Host info condensed */
.host-info-condensed {
display: none;
}
.host-info-expanded {
display: block;
}
}
@media (min-width: 769px) {
.host-info-condensed {
display: block;
}
.host-info-expanded {
display: none;
}
}
/* === MOBILE SCHEDULE STEPPER === */
@media (max-width: 640px) {
.schedule-step-indicator {
flex-direction: column;
align-items: flex-start;
gap: 0;
}
.schedule-step-container {
display: flex;
align-items: center;
width: 100%;
padding: 8px 0;
}
.schedule-step-dot {
flex-shrink: 0;
}
.schedule-step-label {
margin-left: 12px;
font-size: 0.875rem;
color: var(--secondary-text);
}
.schedule-step-connector {
width: 2px;
height: 24px;
margin-left: 15px;
}
/* Form fields full width */
.schedule-modal-step input,
.schedule-modal-step select,
.schedule-modal-step textarea {
width: 100%;
min-height: 44px;
}
}
/* === MOBILE TABLES AS CARDS === */
@media (max-width: 768px) {
.table-card-mobile {
display: block;
}
.table-card-mobile thead {
display: none;
}
.table-card-mobile tbody {
display: flex;
flex-direction: column;
gap: 12px;
}
.table-card-mobile tr {
display: flex;
flex-direction: column;
background: rgba(42, 42, 42, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 16px;
}
.table-card-mobile td {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.table-card-mobile td:last-child {
border-bottom: none;
}
.table-card-mobile td::before {
content: attr(data-label);
font-weight: 600;
color: var(--secondary-text);
font-size: 0.75rem;
text-transform: uppercase;
}
}
/* === MOBILE TASK CARDS === */
@media (max-width: 640px) {
.task-log-card {
padding: 14px;
}
.task-log-card .flex.items-center.gap-3 {
flex-wrap: wrap;
}
.task-log-card-header {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.task-log-card-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 0.75rem;
}
.task-log-card-actions {
display: flex;
gap: 8px;
margin-top: 12px;
width: 100%;
}
.task-log-card-actions button {
flex: 1;
min-height: 40px;
}
}
/* === MOBILE PLAYBOOK CARDS === */
@media (max-width: 640px) {
.playbook-card {
padding: 14px;
}
.playbook-card-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.playbook-card-actions {
display: flex;
gap: 8px;
width: 100%;
margin-top: 12px;
}
.playbook-action-btn {
flex: 1;
min-height: 44px;
justify-content: center;
}
}
/* === MOBILE CALENDAR === */
@media (max-width: 640px) {
.schedule-calendar-day {
min-height: 60px;
padding: 4px;
font-size: 0.7rem;
}
.schedule-calendar-event {
font-size: 0.6rem;
padding: 1px 3px;
}
#schedule-calendar-grid {
gap: 2px;
}
}
/* === MOBILE HELP PAGE === */
@media (max-width: 1024px) {
.help-toc {
display: none;
}
.help-main-content {
width: 100%;
}
}
@media (max-width: 640px) {
.help-card {
padding: 16px;
}
.help-section-title {
font-size: 1.1rem;
}
}
/* === MOBILE FOOTER === */
@media (max-width: 640px) {
footer {
padding: 16px;
}
footer p {
font-size: 0.75rem;
}
}
/* === MOBILE LOADING OVERLAY === */
@media (max-width: 640px) {
#loading-overlay .loading-spinner {
width: 32px;
height: 32px;
}
#loading-overlay p {
font-size: 0.875rem;
}
}
/* === REDUCED MOTION === */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.animate-float,
.animate-pulse,
.animate-pulse-slow {
animation: none !important;
}
}
/* === MOBILE ANIMATIONS OPTIMIZATION === */
@media (max-width: 768px) {
.glass-card:hover {
transform: none;
}
.host-card:hover {
transform: none;
}
.playbook-card:hover {
transform: none;
}
.schedule-card:hover {
transform: none;
}
/* Reduce animation complexity */
.fade-in {
transform: none;
transition: opacity 0.3s ease;
}
}
/* === MOBILE TOAST NOTIFICATIONS === */
@media (max-width: 640px) {
.toast-container {
bottom: 70px;
left: 16px;
right: 16px;
max-width: none;
}
.toast {
width: 100%;
border-radius: 8px;
}
}
/* === MOBILE DATE PICKER === */
@media (max-width: 640px) {
#task-date-calendar {
left: 0;
right: 0;
width: auto;
margin: 0 16px;
}
.task-calendar {
width: 100%;
}
}
/* === KEBAB MENU FOR MOBILE ACTIONS === */
.kebab-menu {
display: none;
position: relative;
}
.kebab-menu-btn {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(55, 65, 81, 0.5);
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s ease;
}
.kebab-menu-btn:hover,
.kebab-menu-btn:active {
background: rgba(124, 58, 237, 0.3);
}
.kebab-menu-dropdown {
position: absolute;
top: 100%;
right: 0;
min-width: 160px;
background: var(--secondary-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
z-index: 50;
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all 0.2s ease;
}
.kebab-menu.open .kebab-menu-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(4px);
}
.kebab-menu-dropdown button {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 12px 16px;
border: none;
background: none;
color: var(--primary-text);
font-size: 0.875rem;
cursor: pointer;
transition: background 0.2s ease;
}
.kebab-menu-dropdown button:hover {
background: rgba(124, 58, 237, 0.1);
}
.kebab-menu-dropdown button:first-child {
border-radius: 8px 8px 0 0;
}
.kebab-menu-dropdown button:last-child {
border-radius: 0 0 8px 8px;
}
@media (max-width: 640px) {
.kebab-menu {
display: block;
}
.desktop-actions {
display: none;
}
}
@media (min-width: 641px) {
.kebab-menu {
display: none;
}
.desktop-actions {
display: flex;
}
}
/* === MOBILE DROPDOWN CLICK SUPPORT === */
.group.dropdown-open > div[class*="absolute"] {
opacity: 1 !important;
visibility: visible !important;
}
/* Disable hover dropdowns on touch devices */
@media (hover: none) {
.group:hover > div[class*="absolute"] {
opacity: 0;
visibility: hidden;
}
.group.dropdown-open > div[class*="absolute"] {
opacity: 1 !important;
visibility: visible !important;
}
}
/* === MOBILE NAV SHOW/HIDE === */
@media (max-width: 768px) {
.mobile-menu-btn {
display: flex;
}
.desktop-nav-links {
display: none;
}
.mobile-nav-overlay,
.mobile-nav-sidebar {
display: block;
}
/* Adjust nav padding for mobile */
nav .max-w-7xl {
padding-left: 16px;
padding-right: 16px;
}
}
/* === MOBILE-FIRST GRID ADJUSTMENTS === */
@media (max-width: 640px) {
.max-w-7xl {
padding-left: 16px;
padding-right: 16px;
}
/* Single column on mobile */
.grid-cols-1.md\\:grid-cols-2.lg\\:grid-cols-4 {
grid-template-columns: 1fr;
}
.grid-cols-1.lg\\:grid-cols-3 {
grid-template-columns: 1fr;
}
}
/* === SWIPE HINT === */
.swipe-hint {
display: none;
}
@media (max-width: 768px) {
.swipe-hint {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px;
background: rgba(124, 58, 237, 0.1);
border-radius: 8px;
margin-bottom: 12px;
font-size: 0.75rem;
color: var(--secondary-text);
}
.swipe-hint i {
animation: swipeHint 1.5s ease-in-out infinite;
}
@keyframes swipeHint {
0%, 100% { transform: translateX(0); }
50% { transform: translateX(8px); }
}
}
/* === BOTTOM ACTION BAR FOR MOBILE === */
.mobile-action-bar {
display: none;
}
@media (max-width: 640px) {
.mobile-action-bar {
display: flex;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--secondary-bg);
border-top: 1px solid var(--border-color);
padding: 8px 16px;
padding-bottom: calc(8px + env(safe-area-inset-bottom));
z-index: 40;
gap: 8px;
}
.mobile-action-bar button {
flex: 1;
min-height: 44px;
border-radius: 8px;
font-size: 0.75rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
}
/* Add bottom padding to body for action bar */
body {
padding-bottom: 70px;
}
}
/* === LIGHT THEME MOBILE ADJUSTMENTS === */
body.light-theme .mobile-nav-sidebar {
background: #ffffff;
border-color: #e5e5e5;
}
body.light-theme .mobile-nav-links button,
body.light-theme .mobile-nav-links a {
color: #4b5563;
}
body.light-theme .mobile-nav-links button:hover,
body.light-theme .mobile-nav-links button:active,
body.light-theme .mobile-nav-links a:hover,
body.light-theme .mobile-nav-links a:active {
color: #1f2937;
}
body.light-theme .mobile-nav-links button.active,
body.light-theme .mobile-nav-links a.active {
color: #7c3aed;
}
body.light-theme .mobile-action-bar {
background: #ffffff;
border-color: #e5e5e5;
}
body.light-theme .kebab-menu-dropdown {
background: #ffffff;
border-color: #e5e5e5;
}
body.light-theme .kebab-menu-dropdown button {
color: #1f2937;
}
body.light-theme .kebab-menu-dropdown button:hover {
background: rgba(124, 58, 237, 0.1);
}
/* === SCROLLBAR HIDE UTILITY === */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* === SAFE AREA INSETS FOR NOTCHED PHONES === */
@supports (padding: max(0px)) {
.safe-area-bottom {
padding-bottom: max(16px, env(safe-area-inset-bottom));
}
.safe-area-top {
padding-top: max(16px, env(safe-area-inset-top));
}
}
/* === PULL TO REFRESH INDICATOR (visual only) === */
.ptr-indicator {
display: none;
position: fixed;
top: 60px;
left: 50%;
transform: translateX(-50%);
background: var(--accent-color);
padding: 8px 16px;
border-radius: 20px;
font-size: 0.75rem;
z-index: 100;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
/* === LANDSCAPE ORIENTATION ADJUSTMENTS === */
@media (max-height: 500px) and (orientation: landscape) {
.hero-section {
padding-top: 60px;
padding-bottom: 20px;
}
.hero-section h1 {
font-size: 1.5rem;
margin-bottom: 8px;
}
.hero-section p {
font-size: 0.875rem;
margin-bottom: 12px;
}
.metric-card {
padding: 12px;
}
}
</style>
</head>
<body>
<!-- Login Screen - shown when not authenticated -->
<div id="login-screen" class="fixed inset-0 bg-black z-[100] flex items-center justify-center hidden">
<div class="max-w-md w-full mx-4">
<!-- Login Form -->
<div id="login-form-container" class="glass-card p-8">
<div class="text-center mb-8">
<div class="w-16 h-16 bg-gradient-to-br from-purple-600 to-blue-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
<i class="fas fa-server text-white text-2xl"></i>
</div>
<h1 class="text-2xl font-bold gradient-text mb-2">Homelab Dashboard</h1>
<p class="text-gray-400">Connectez-vous pour continuer</p>
</div>
<form id="login-form" onsubmit="handleLogin(event)" class="space-y-6">
<div>
<label for="login-username" class="block text-sm font-medium text-gray-300 mb-2">
<i class="fas fa-user mr-2"></i>Nom d'utilisateur
</label>
<input
type="text"
id="login-username"
name="username"
required
autocomplete="username"
class="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition-colors"
placeholder="Entrez votre nom d'utilisateur"
>
</div>
<div>
<label for="login-password" class="block text-sm font-medium text-gray-300 mb-2">
<i class="fas fa-lock mr-2"></i>Mot de passe
</label>
<div class="relative">
<input
type="password"
id="login-password"
name="password"
required
autocomplete="current-password"
class="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition-colors pr-12"
placeholder="Entrez votre mot de passe"
>
<button
type="button"
onclick="togglePasswordVisibility('login-password', this)"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300"
>
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div id="login-error" class="hidden text-red-400 text-sm bg-red-900/20 border border-red-800 rounded-lg p-3">
<i class="fas fa-exclamation-circle mr-2"></i>
<span id="login-error-text"></span>
</div>
<button
type="submit"
id="login-submit-btn"
class="w-full btn-primary py-3 flex items-center justify-center gap-2"
>
<i class="fas fa-sign-in-alt"></i>
<span>Se connecter</span>
</button>
</form>
</div>
<!-- Setup Form (first user creation) -->
<div id="setup-form-container" class="glass-card p-8 hidden">
<div class="text-center mb-8">
<div class="w-16 h-16 bg-gradient-to-br from-green-600 to-emerald-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
<i class="fas fa-user-plus text-white text-2xl"></i>
</div>
<h1 class="text-2xl font-bold gradient-text mb-2">Configuration Initiale</h1>
<p class="text-gray-400">Créez votre compte administrateur</p>
</div>
<form id="setup-form" onsubmit="handleSetup(event)" class="space-y-5">
<div>
<label for="setup-username" class="block text-sm font-medium text-gray-300 mb-2">
<i class="fas fa-user mr-2"></i>Nom d'utilisateur *
</label>
<input
type="text"
id="setup-username"
name="username"
required
minlength="3"
maxlength="50"
autocomplete="username"
class="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition-colors"
placeholder="admin"
>
</div>
<div>
<label for="setup-password" class="block text-sm font-medium text-gray-300 mb-2">
<i class="fas fa-lock mr-2"></i>Mot de passe *
</label>
<div class="relative">
<input
type="password"
id="setup-password"
name="password"
required
minlength="6"
autocomplete="new-password"
class="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition-colors pr-12"
placeholder="Minimum 6 caractères"
>
<button
type="button"
onclick="togglePasswordVisibility('setup-password', this)"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300"
>
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div>
<label for="setup-password-confirm" class="block text-sm font-medium text-gray-300 mb-2">
<i class="fas fa-lock mr-2"></i>Confirmer le mot de passe *
</label>
<input
type="password"
id="setup-password-confirm"
name="password_confirm"
required
minlength="6"
autocomplete="new-password"
class="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition-colors"
placeholder="Confirmez votre mot de passe"
>
</div>
<div>
<label for="setup-email" class="block text-sm font-medium text-gray-300 mb-2">
<i class="fas fa-envelope mr-2"></i>Email (optionnel)
</label>
<input
type="email"
id="setup-email"
name="email"
autocomplete="email"
class="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition-colors"
placeholder="admin@example.com"
>
</div>
<div>
<label for="setup-display-name" class="block text-sm font-medium text-gray-300 mb-2">
<i class="fas fa-id-card mr-2"></i>Nom d'affichage (optionnel)
</label>
<input
type="text"
id="setup-display-name"
name="display_name"
maxlength="100"
class="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition-colors"
placeholder="Administrateur"
>
</div>
<div id="setup-error" class="hidden text-red-400 text-sm bg-red-900/20 border border-red-800 rounded-lg p-3">
<i class="fas fa-exclamation-circle mr-2"></i>
<span id="setup-error-text"></span>
</div>
<button
type="submit"
id="setup-submit-btn"
class="w-full btn-primary py-3 flex items-center justify-center gap-2"
>
<i class="fas fa-user-plus"></i>
<span>Créer le compte</span>
</button>
</form>
</div>
<p class="text-center text-gray-500 text-sm mt-6">
Homelab Automation Dashboard v1.0
</p>
</div>
</div>
<!-- Main Content Wrapper -->
<div id="main-content">
<!-- Loading indicator - will be hidden when JS loads -->
<div id="page-loading" style="position:fixed;inset:0;background:#0a0a0a;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:9999;">
<div style="width:50px;height:50px;border:3px solid #333;border-top-color:#7c3aed;border-radius:50%;animation:spin 1s linear infinite;"></div>
<p style="color:#a1a1aa;margin-top:16px;font-family:sans-serif;">Chargement du Dashboard...</p>
<style>@keyframes spin{to{transform:rotate(360deg);}}</style>
</div>
<script>
// Hide loading indicator when page is ready
window.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
var loader = document.getElementById('page-loading');
if (loader) loader.style.display = 'none';
}, 100);
});
// Fallback: hide after 10 seconds anyway
setTimeout(function() {
var loader = document.getElementById('page-loading');
if (loader) loader.style.display = 'none';
}, 10000);
</script>
<!-- Mobile Navigation Overlay -->
<div id="mobile-nav-overlay" class="mobile-nav-overlay" onclick="closeMobileNav()"></div>
<!-- Mobile Navigation Sidebar -->
<aside id="mobile-nav-sidebar" class="mobile-nav-sidebar">
<div class="mobile-nav-header">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 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-sm"></i>
</div>
<span class="font-bold gradient-text">Homelab</span>
</div>
<button type="button" onclick="closeMobileNav()" class="touch-target text-gray-400 hover:text-white">
<i class="fas fa-times text-lg"></i>
</button>
</div>
<nav class="mobile-nav-links">
<button type="button" data-page="dashboard" class="mobile-nav-link" onclick="mobileNavigateTo('dashboard'); return false;">
<i class="fas fa-tachometer-alt"></i>
<span>Dashboard</span>
</button>
<button type="button" data-page="hosts" class="mobile-nav-link" onclick="mobileNavigateTo('hosts'); return false;">
<i class="fas fa-server"></i>
<span>Hosts</span>
</button>
<button type="button" data-page="playbooks" class="mobile-nav-link" onclick="mobileNavigateTo('playbooks'); return false;">
<i class="fas fa-book"></i>
<span>Playbooks</span>
</button>
<button type="button" data-page="tasks" class="mobile-nav-link" onclick="mobileNavigateTo('tasks'); return false;">
<i class="fas fa-tasks"></i>
<span>Tasks</span>
</button>
<button type="button" data-page="schedules" class="mobile-nav-link" onclick="mobileNavigateTo('schedules'); return false;">
<i class="fas fa-calendar-alt"></i>
<span>Schedules</span>
</button>
<button type="button" data-page="logs" class="mobile-nav-link" onclick="mobileNavigateTo('logs'); return false;">
<i class="fas fa-file-alt"></i>
<span>Logs</span>
</button>
<button type="button" data-page="docker" class="mobile-nav-link" onclick="mobileNavigateTo('docker'); return false;">
<i class="fab fa-docker"></i>
<span>Docker</span>
</button>
<button type="button" data-page="alerts" class="mobile-nav-link" onclick="mobileNavigateTo('alerts'); return false;">
<i class="fas fa-bell"></i>
<span>Alertes</span>
</button>
<button type="button" data-page="configuration" class="mobile-nav-link" onclick="mobileNavigateTo('configuration'); return false;">
<i class="fas fa-sliders-h"></i>
<span>Configuration</span>
</button>
</nav>
<div class="p-4 border-t border-gray-700 space-y-3">
<button type="button" id="mobile-theme-toggle" onclick="toggleTheme()" class="w-full flex items-center justify-center gap-2 p-3 bg-gray-800 rounded-lg hover:bg-gray-700 transition-colors">
<i class="fas fa-moon text-gray-300" id="mobile-theme-icon"></i>
<span class="text-sm text-gray-300" id="mobile-theme-label">Thème sombre</span>
</button>
<button type="button" onclick="handleLogout(); closeMobileNav();" class="w-full flex items-center justify-center gap-2 p-3 bg-red-900/30 border border-red-800 rounded-lg hover:bg-red-900/50 transition-colors">
<i class="fas fa-sign-out-alt text-red-400"></i>
<span class="text-sm text-red-400">Déconnexion</span>
</button>
</div>
</aside>
<!-- 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-4 sm:px-6 py-3 sm:py-4">
<div class="flex items-center justify-between">
<!-- Logo -->
<div class="flex items-center space-x-3 cursor-pointer" onclick="navigateTo('dashboard')">
<div class="w-9 h-9 sm:w-10 sm: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-base sm:text-lg"></i>
</div>
<h1 class="text-lg sm:text-xl font-bold gradient-text hidden sm:block">Homelab Dashboard</h1>
<h1 class="text-lg font-bold gradient-text sm:hidden">Homelab</h1>
</div>
<!-- Desktop Navigation -->
<div class="desktop-nav-links">
<a href="#" data-page="dashboard" class="nav-link text-gray-300 hover:text-white transition-colors" title="Dashboard" aria-label="Dashboard">
<i class="fas fa-gauge-high"></i>
<span class="sr-only">Dashboard</span>
</a>
<a href="#" data-page="hosts" class="nav-link text-gray-300 hover:text-white transition-colors" title="Hosts" aria-label="Hosts">
<i class="fas fa-server"></i>
<span class="sr-only">Hosts</span>
</a>
<a href="#" data-page="playbooks" class="nav-link text-gray-300 hover:text-white transition-colors" title="Playbooks" aria-label="Playbooks">
<i class="fas fa-book"></i>
<span class="sr-only">Playbooks</span>
</a>
<a href="#" data-page="tasks" class="nav-link text-gray-300 hover:text-white transition-colors" title="Tasks" aria-label="Tasks">
<i class="fas fa-list-check"></i>
<span class="sr-only">Tasks</span>
</a>
<a href="#" data-page="schedules" class="nav-link text-gray-300 hover:text-white transition-colors" title="Schedules" aria-label="Schedules">
<i class="fas fa-calendar-alt"></i>
<span class="sr-only">Schedules</span>
</a>
<a href="#" data-page="logs" class="nav-link text-gray-300 hover:text-white transition-colors" title="Logs" aria-label="Logs">
<i class="fas fa-file-alt"></i>
<span class="sr-only">Logs</span>
</a>
<a href="#" data-page="docker" class="nav-link text-gray-300 hover:text-white transition-colors" title="Docker" aria-label="Docker">
<i class="fab fa-docker"></i>
<span class="sr-only">Docker</span>
</a>
<a href="#" data-page="configuration" class="nav-link text-gray-300 hover:text-white transition-colors" title="Configuration" aria-label="Configuration">
<i class="fas fa-sliders-h"></i>
<span class="sr-only">Configuration</span>
</a>
<button id="theme-toggle" class="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors touch-target">
<i class="fas fa-moon text-gray-300"></i>
</button>
<button id="alerts-button" class="relative p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors touch-target" onclick="navigateTo('alerts')" title="Alertes">
<i class="fas fa-bell text-gray-300"></i>
<span id="alerts-badge" class="hidden absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 bg-red-600 text-white text-[10px] leading-[18px] rounded-full text-center"></span>
</button>
<!-- User Menu -->
<div class="relative group">
<button class="flex items-center gap-2 p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors">
<i class="fas fa-user-circle text-gray-300"></i>
<span id="current-user-name" class="text-sm text-gray-300 hidden lg:inline"></span>
<i class="fas fa-chevron-down text-xs text-gray-500"></i>
</button>
<div class="absolute right-0 mt-2 w-48 bg-gray-800 border border-gray-700 rounded-lg shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-50">
<div class="p-3 border-b border-gray-700">
<p class="text-sm font-medium text-white" id="user-menu-name">Utilisateur</p>
<p class="text-xs text-gray-400" id="current-user-role">Admin</p>
</div>
<div class="p-2">
<button type="button" onclick="navigateTo('configuration')" class="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-200 hover:bg-gray-700 rounded-lg transition-colors">
<i class="fas fa-sliders-h text-gray-300"></i>
<span>Configuration</span>
</button>
<button type="button" onclick="navigateTo('help')" class="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-200 hover:bg-gray-700 rounded-lg transition-colors">
<i class="fas fa-question-circle text-gray-300"></i>
<span>Centre d'aide</span>
</button>
<button onclick="handleLogout()" class="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-400 hover:bg-gray-700 rounded-lg transition-colors">
<i class="fas fa-sign-out-alt"></i>
<span>Déconnexion</span>
</button>
</div>
</div>
</div>
</div>
<!-- Mobile Menu Button -->
<div class="flex items-center gap-2 md:hidden">
<button type="button" id="theme-toggle-mobile" class="touch-target rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors">
<i class="fas fa-moon text-gray-300"></i>
</button>
<button type="button" id="mobile-menu-btn" class="mobile-menu-btn" onclick="toggleMobileNav()" aria-label="Menu">
<div class="hamburger-icon">
<span></span>
<span></span>
<span></span>
</div>
</button>
</div>
</div>
</div>
</nav>
<!-- ==================== PAGE: DASHBOARD ==================== -->
<section id="page-dashboard" class="page-section active">
<!-- Hero Section -->
<div class="hero-section pt-20 sm:pt-24 pb-8 sm:pb-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-16">
<h1 class="text-3xl sm:text-4xl md:text-5xl font-bold mb-4 sm:mb-6 gradient-text animate-float">
Automation Dashboard
</h1>
<p class="text-base sm:text-lg md:text-xl text-gray-400 mb-6 sm:mb-8 max-w-2xl mx-auto px-4">
Gérez votre homelab avec puissance et élégance. Surveillance, automatisation et contrôle en temps réel.
</p>
<!-- Mobile: Stack buttons vertically -->
<div class="flex flex-col sm:flex-row justify-center gap-3 sm:gap-4 px-4 sm:px-0">
<button type="button" class="btn-primary flex items-center justify-center" onclick="dashboard.showQuickActions()">
<i class="fas fa-bolt mr-2"></i>
Actions Rapides
</button>
<button type="button" class="px-6 py-3 border border-gray-600 rounded-lg text-gray-300 hover:border-purple-500 hover:text-white transition-all flex items-center justify-center" onclick="navigateTo('hosts')">
<i class="fas fa-server mr-2"></i>
Voir les Hosts
</button>
</div>
</div>
<!-- Metrics Overview - 2 columns on mobile, 4 on desktop -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 sm:gap-6 mb-8 sm:mb-16">
<div class="metric-card fade-in">
<div class="text-2xl sm:text-3xl font-bold text-green-400 mb-1 sm:mb-2" id="online-hosts">12</div>
<div class="text-xs sm:text-sm text-gray-400">Hosts En Ligne</div>
</div>
<div class="metric-card fade-in">
<div class="text-2xl sm:text-3xl font-bold text-blue-400 mb-1 sm:mb-2" id="total-tasks">48</div>
<div class="text-xs sm:text-sm text-gray-400">Tâches Exécutées</div>
</div>
<div class="metric-card fade-in">
<div class="text-2xl sm:text-3xl font-bold text-purple-400 mb-1 sm:mb-2" id="success-rate">98.5%</div>
<div class="text-xs sm:text-sm text-gray-400">Taux de Succès</div>
</div>
<div class="metric-card fade-in">
<div class="text-2xl sm:text-3xl font-bold text-orange-400 mb-1 sm:mb-2" id="uptime">99.9%</div>
<div class="text-xs sm:text-sm text-gray-400">Disponibilité</div>
</div>
</div>
</div>
</div>
<!-- Dashboard Content -->
<div id="dashboard" class="py-8 sm:py-16 bg-gradient-to-b from-gray-900 to-black">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<h2 class="text-2xl sm:text-3xl font-bold mb-8 sm:mb-12 text-center gradient-text fade-in">Tableau de Bord</h2>
<!-- Mobile: Quick Actions first, then schedules -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-8">
<!-- Console Ad-Hoc Widget + Quick Actions - Stacked on right column -->
<div class="flex flex-col gap-4 sm:gap-6 order-1 lg:order-2">
<!-- Console Ad-Hoc Ansible Widget -->
<div class="glass-card p-4 sm:p-6 fade-in">
<div class="flex items-center justify-between mb-4">
<h3 class="text-base sm:text-lg font-semibold flex items-center">
<i class="fas fa-terminal text-purple-400 mr-2"></i>
Console Ad-Hoc
</h3>
<div class="flex items-center gap-2">
<span id="adhoc-widget-count" class="text-xs text-gray-500 hidden sm:inline"></span>
</div>
</div>
<!-- Bouton principal Console -->
<button type="button" onclick="dashboard.showAdHocConsole()"
class="w-full p-3 sm:p-4 bg-gradient-to-r from-purple-600 to-purple-700 rounded-lg hover:from-purple-500 hover:to-purple-600 transition-all flex items-center justify-center mb-4 group">
<i class="fas fa-terminal mr-2 sm:mr-3 text-sm sm:text-base group-hover:animate-pulse"></i>
<span class="text-sm sm:text-base font-medium">Console Ad-Hoc</span>
</button>
<!-- Mini stats -->
<div class="grid grid-cols-3 gap-2 mb-4">
<div class="text-center p-2 bg-gray-800/50 rounded">
<div class="text-sm sm:text-base font-bold text-green-400" id="adhoc-widget-success">0</div>
<div class="text-[9px] sm:text-xs text-gray-500">Succès</div>
</div>
<div class="text-center p-2 bg-gray-800/50 rounded">
<div class="text-sm sm:text-base font-bold text-red-400" id="adhoc-widget-failed">0</div>
<div class="text-[9px] sm:text-xs text-gray-500">Échecs</div>
</div>
<div class="text-center p-2 bg-gray-800/50 rounded">
<div class="text-sm sm:text-base font-bold text-purple-400" id="adhoc-widget-total">0</div>
<div class="text-[9px] sm:text-xs text-gray-500">Total</div>
</div>
</div>
<!-- Historique récent -->
<div class="border-t border-gray-700/50 pt-3">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-gray-400 uppercase tracking-wide">
<i class="fas fa-history mr-1"></i>Dernières exécutions
</span>
</div>
<div id="adhoc-widget-history" class="space-y-2 max-h-48 overflow-y-auto">
<p class="text-xs text-gray-500 text-center py-3">
<i class="fas fa-spinner fa-spin mr-1"></i>Chargement...
</p>
</div>
<button type="button" onclick="dashboard.loadMoreAdhocHistory()"
id="adhoc-widget-load-more"
class="w-full mt-3 p-2 text-xs text-purple-400 hover:text-purple-300 hover:bg-purple-900/20 rounded transition-colors hidden">
<i class="fas fa-chevron-down mr-1"></i>Charger plus
</button>
</div>
</div>
<!-- Quick Actions - Now below Ad-Hoc widget -->
<div class="glass-card p-4 sm:p-6 fade-in">
<h3 class="text-lg sm:text-xl font-semibold mb-4 sm:mb-6">Actions Rapides</h3>
<!-- Mobile: 2x2 grid, Desktop: vertical stack -->
<div class="grid grid-cols-2 lg:grid-cols-1 gap-2 sm:gap-4">
<button type="button" class="w-full p-3 sm: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 flex items-center" onclick="executeTask('upgrade-all')">
<i class="fas fa-arrow-up mr-2 sm:mr-3 text-sm sm:text-base"></i>
<span class="text-xs sm:text-sm lg:text-base">Mise à jour</span>
</button>
<button type="button" class="w-full p-3 sm: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 flex items-center" onclick="executeTask('reboot-all')">
<i class="fas fa-redo mr-2 sm:mr-3 text-sm sm:text-base"></i>
<span class="text-xs sm:text-sm lg:text-base">Redémarrer</span>
</button>
<button type="button" class="w-full p-3 sm: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 flex items-center" onclick="executeTask('health-check')">
<i class="fas fa-heartbeat mr-2 sm:mr-3 text-sm sm:text-base"></i>
<span class="text-xs sm:text-sm lg:text-base">Santé</span>
</button>
<button type="button" class="w-full p-3 sm: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 flex items-center" onclick="executeTask('backup')">
<i class="fas fa-save mr-2 sm:mr-3 text-sm sm:text-base"></i>
<span class="text-xs sm:text-sm lg:text-base">Backup</span>
</button>
</div>
</div>
</div>
<!-- Schedules Widget -->
<div class="lg:col-span-2 order-2 lg:order-1">
<div class="glass-card p-4 sm:p-6 h-full flex flex-col fade-in">
<div class="flex items-center justify-between mb-4">
<h3 class="text-base sm:text-lg font-semibold">
<i class="fas fa-calendar-alt text-purple-400 mr-2"></i>Planificateur
</h3>
<a href="#" data-page="schedules" class="nav-link text-purple-400 text-xs sm:text-sm hover:underline">Voir tout →</a>
</div>
<!-- Stats mini -->
<div class="grid grid-cols-3 gap-2 mb-4">
<div class="text-center p-2 bg-gray-800/50 rounded">
<div class="text-base sm:text-lg font-bold text-green-400" id="dashboard-schedules-active">0</div>
<div class="text-[10px] sm:text-xs text-gray-500">Actifs</div>
</div>
<div class="text-center p-2 bg-gray-800/50 rounded">
<div class="text-base sm:text-lg font-bold text-blue-400" id="dashboard-schedules-next">--</div>
<div class="text-[10px] sm:text-xs text-gray-500">Prochaine</div>
</div>
<div class="text-center p-2 bg-gray-800/50 rounded">
<div class="text-base sm:text-lg font-bold text-red-400" id="dashboard-schedules-failures">0</div>
<div class="text-[10px] sm:text-xs text-gray-500">Échecs 24h</div>
</div>
</div>
<!-- Next executions -->
<div id="dashboard-upcoming-schedules" class="space-y-2 text-xs sm:text-sm flex-1 min-h-[100px]">
<p class="text-gray-500 text-center py-4">Chargement...</p>
</div>
<button type="button" onclick="showCreateScheduleModal()" class="w-full mt-4 p-3 sm:p-2 bg-purple-600 hover:bg-purple-500 rounded-lg text-sm transition-colors touch-target">
<i class="fas fa-plus mr-2"></i>Nouveau schedule
</button>
</div>
</div>
<!-- Hosts Management (Moved down) -->
<div class="lg:col-span-2 order-3">
<div class="glass-card p-4 sm:p-6 fade-in">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h3 class="text-lg sm:text-xl font-semibold">Gestion des Hosts</h3>
<div class="flex items-center gap-2">
<button type="button" class="btn-primary text-sm sm:text-base flex-1 sm:flex-none" onclick="dashboard.addHost()">
<i class="fas fa-plus mr-1 sm:mr-2"></i>
<span class="hidden sm:inline">Ajouter Host</span>
<span class="sm:hidden">Ajouter</span>
</button>
<div class="relative group">
<button type="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 type="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 type="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 type="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 type="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>
</div>
</div>
</div>
</section>
<!-- END PAGE: DASHBOARD -->
<!-- ==================== PAGE: HOSTS ==================== -->
<section id="page-hosts" class="page-section">
<div class="pt-20 sm:pt-24 pb-8 sm:pb-16 min-h-screen bg-gradient-to-b from-gray-900 to-black">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<div class="text-center mb-6 sm:mb-12">
<h1 class="text-2xl sm:text-3xl md:text-4xl font-bold mb-2 sm:mb-4 gradient-text">
<i class="fas fa-server mr-2 sm:mr-3"></i>Gestion des Hosts
</h1>
<p class="text-sm sm:text-base text-gray-400 px-4">Gérez et surveillez tous vos serveurs depuis un seul endroit</p>
</div>
<div class="glass-card p-4 sm:p-6 fade-in">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h3 class="text-lg sm:text-xl font-semibold">Inventaire des Hosts</h3>
<div class="flex items-center gap-2">
<button type="button" class="btn-primary flex-1 sm:flex-none text-sm touch-target" onclick="dashboard.addHost()">
<i class="fas fa-plus mr-1 sm:mr-2"></i>
<span class="hidden sm:inline">Ajouter Host</span>
<span class="sm:hidden">Ajouter</span>
</button>
<div class="relative group">
<button type="button" class="px-3 sm:px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors flex items-center text-sm touch-target">
<i class="fas fa-layer-group mr-1 sm:mr-2"></i>
<span class="hidden sm:inline">Groupes</span>
<i class="fas fa-chevron-down ml-1 sm: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 type="button" onclick="dashboard.showAddGroupModal('env')" class="w-full px-4 py-3 text-left hover:bg-gray-700 flex items-center touch-target">
<i class="fas fa-plus text-green-400 mr-3 w-4"></i>
Ajouter environnement
</button>
<button type="button" onclick="dashboard.showManageGroupsModal('env')" class="w-full px-4 py-3 text-left hover:bg-gray-700 flex items-center touch-target">
<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 type="button" onclick="dashboard.showAddGroupModal('role')" class="w-full px-4 py-3 text-left hover:bg-gray-700 flex items-center touch-target">
<i class="fas fa-plus text-green-400 mr-3 w-4"></i>
Ajouter rôle
</button>
<button type="button" onclick="dashboard.showManageGroupsModal('role')" class="w-full px-4 py-3 text-left hover:bg-gray-700 flex items-center touch-target">
<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-3 sm: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-20 sm:pt-24 pb-8 sm:pb-16 min-h-screen bg-gradient-to-b from-gray-900 to-black">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<!-- Header avec effet glow violet -->
<div class="text-center mb-6 sm:mb-12">
<h1 class="text-2xl sm:text-3xl md:text-4xl font-bold mb-2 sm:mb-4 gradient-text">
<i class="fas fa-book mr-2 sm:mr-3"></i>Gestion des Playbooks
</h1>
<p class="text-sm sm:text-base text-gray-400 max-w-2xl mx-auto px-4">Gérez vos scripts d'automatisation Ansible depuis un seul endroit</p>
</div>
<div class="glass-card p-4 sm:p-6 fade-in">
<!-- Toolbar -->
<div class="flex flex-col gap-4 mb-4 sm:mb-6">
<!-- Row 1: Search (full width on mobile) -->
<div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
<div class="relative flex-1 sm:flex-none">
<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-full sm:w-64"
placeholder="Rechercher un playbook..."
oninput="dashboard.filterPlaybooks(this.value)">
</div>
<span id="playbooks-count" class="text-xs sm:text-sm text-gray-400">
<i class="fas fa-file-code mr-1"></i>0 playbooks
</span>
</div>
<!-- Row 2: Action buttons -->
<div class="flex items-center gap-2 sm:gap-3">
<button type="button" onclick="dashboard.refreshPlaybooks()"
class="px-3 sm:px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors text-sm flex items-center gap-2 touch-target">
<i class="fas fa-sync-alt"></i>
<span class="hidden sm:inline">Rafraîchir</span>
</button>
<button type="button" onclick="dashboard.showCreatePlaybookModal()"
class="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-purple-600 rounded-lg hover:bg-purple-500 transition-colors text-sm flex items-center justify-center gap-2 shadow-lg shadow-purple-500/20 touch-target">
<i class="fas fa-plus"></i>
<span class="hidden sm:inline">Nouveau Playbook</span>
<span class="sm:hidden">Nouveau</span>
</button>
</div>
</div>
<!-- Filtres par catégorie - scrollable on mobile -->
<div id="playbook-category-filters" class="flex items-center gap-2 mb-4 sm:mb-6 p-3 bg-gray-800/50 rounded-lg overflow-x-auto scrollbar-hide">
<span class="text-xs text-gray-500 mr-2"><i class="fas fa-filter mr-1"></i>Catégorie:</span>
<button type="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 type="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 type="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 type="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 type="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 type="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-20 sm:pt-24 pb-8 sm:pb-16 min-h-screen bg-black">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<h2 class="text-2xl sm:text-3xl font-bold mb-6 sm:mb-12 text-center gradient-text fade-in">Gestion des Tâches</h2>
<div class="glass-card p-4 sm:p-6 fade-in">
<!-- Header avec filtres -->
<div class="flex flex-col gap-4 mb-4 sm:mb-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 sm:gap-4">
<h3 class="text-lg sm:text-xl font-semibold">Tâches</h3>
<span id="tasks-count-badge" class="px-2 sm:px-3 py-1 bg-purple-600 text-xs sm:text-sm rounded-full">0</span>
</div>
</div>
<!-- Boutons de filtrage par statut - horizontal scroll on mobile -->
<div class="overflow-x-auto scrollbar-hide -mx-4 px-4 sm:mx-0 sm:px-0">
<div class="flex gap-2 min-w-max sm:min-w-0 sm:flex-wrap" id="status-filters">
<button type="button" onclick="dashboard.filterTasksByStatus('all')" data-status="all"
class="task-filter-btn active px-3 sm:px-4 py-2 rounded-lg text-xs sm:text-sm font-medium transition-all bg-purple-600 whitespace-nowrap touch-target">
<i class="fas fa-list mr-1 sm: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 type="button" onclick="dashboard.filterTasksByStatus('running')" data-status="running"
class="task-filter-btn px-3 sm:px-4 py-2 rounded-lg text-xs sm:text-sm font-medium transition-all bg-gray-700 hover:bg-blue-600 whitespace-nowrap touch-target">
<i class="fas fa-spinner fa-spin mr-1 sm: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 type="button" onclick="dashboard.filterTasksByStatus('completed')" data-status="completed"
class="task-filter-btn px-3 sm:px-4 py-2 rounded-lg text-xs sm:text-sm font-medium transition-all bg-gray-700 hover:bg-green-600 whitespace-nowrap touch-target">
<i class="fas fa-check-circle mr-1 sm: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 type="button" onclick="dashboard.filterTasksByStatus('failed')" data-status="failed"
class="task-filter-btn px-3 sm:px-4 py-2 rounded-lg text-xs sm:text-sm font-medium transition-all bg-gray-700 hover:bg-red-600 whitespace-nowrap touch-target">
<i class="fas fa-times-circle mr-1 sm: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>
</div>
<!-- Filtrage par date (calendrier) -->
<div class="flex flex-col sm:flex-row sm:flex-wrap items-stretch sm:items-center gap-3 mb-4 sm:mb-6 p-3 sm:p-4 bg-gray-800/50 rounded-lg">
<span class="text-xs sm: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>
<!-- Filtrage par heure -->
<div class="px-3 py-2 border-t border-gray-800">
<div class="flex items-center gap-2 text-xs text-gray-400 mb-2">
<i class="fas fa-clock"></i>
<span>Plage horaire (optionnel)</span>
</div>
<div class="flex items-center gap-2">
<input type="time" id="task-cal-hour-start"
class="flex-1 px-2 py-1.5 bg-gray-800 border border-gray-700 rounded text-xs text-gray-200 focus:border-purple-500 focus:outline-none"
placeholder="Début">
<span class="text-gray-500 text-xs">à</span>
<input type="time" id="task-cal-hour-end"
class="flex-1 px-2 py-1.5 bg-gray-800 border border-gray-700 rounded text-xs text-gray-200 focus:border-purple-500 focus:outline-none"
placeholder="Fin">
</div>
</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 type="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 type="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 type="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: SCHEDULES ==================== -->
<section id="page-schedules" class="page-section">
<div class="pt-20 sm:pt-24 pb-8 sm:pb-16 min-h-screen bg-gradient-to-b from-gray-900 to-black">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<!-- Header -->
<div class="text-center mb-6 sm:mb-8">
<h1 class="text-2xl sm:text-3xl md:text-4xl font-bold mb-2 sm:mb-4 gradient-text">
<i class="fas fa-calendar-alt mr-2 sm:mr-3"></i>Planificateur
</h1>
<p class="text-sm sm:text-base text-gray-400 max-w-2xl mx-auto px-4">Planifiez et orchestrez vos playbooks - Exécutions automatiques</p>
</div>
<!-- Actions Header - Stack on mobile -->
<div class="flex flex-col gap-3 sm:gap-4 mb-4 sm:mb-6">
<div class="flex flex-col sm:flex-row gap-2 sm:gap-4">
<button type="button" onclick="showCreateScheduleModal()" class="btn-primary flex-1 sm:flex-none touch-target">
<i class="fas fa-plus mr-2"></i>Nouveau Schedule
</button>
<button type="button" onclick="dashboard.refreshSchedules()" class="px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors touch-target">
<i class="fas fa-sync-alt mr-2"></i><span class="hidden sm:inline">Rafraîchir</span><span class="sm:hidden">Refresh</span>
</button>
<select id="schedule-view-toggle" onchange="dashboard.toggleScheduleView(this.value)" class="px-4 py-2 bg-gray-700 rounded-lg border border-gray-600 text-sm touch-target">
<option value="list">Vue : Liste</option>
<option value="calendar">Vue : Calendrier</option>
</select>
</div>
</div>
<!-- Stats Cards - 2x2 on mobile -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4 mb-4 sm:mb-8">
<div class="glass-card p-3 sm:p-4 text-center">
<div class="text-xl sm:text-2xl font-bold text-green-400" id="schedules-active-count">0</div>
<div class="text-[10px] sm:text-sm text-gray-400">Actifs</div>
</div>
<div class="glass-card p-3 sm:p-4 text-center">
<div class="text-xl sm:text-2xl font-bold text-orange-400" id="schedules-paused-count">0</div>
<div class="text-[10px] sm:text-sm text-gray-400">En pause</div>
</div>
<div class="glass-card p-3 sm:p-4 text-center">
<div class="text-xl sm:text-2xl font-bold text-blue-400" id="schedules-next-run">--:--</div>
<div class="text-[10px] sm:text-sm text-gray-400">Prochaine</div>
</div>
<div class="glass-card p-3 sm:p-4 text-center">
<div class="text-xl sm:text-2xl font-bold text-red-400" id="schedules-failures-24h">0</div>
<div class="text-[10px] sm:text-sm text-gray-400">Échecs 24h</div>
</div>
</div>
<!-- Filters - Stack on mobile -->
<div class="glass-card p-3 sm:p-4 mb-4 sm:mb-6">
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4">
<!-- Filter buttons - scrollable on mobile -->
<div class="flex items-center gap-2 overflow-x-auto scrollbar-hide">
<span class="text-xs sm:text-sm text-gray-400 whitespace-nowrap"><i class="fas fa-filter mr-1"></i>Filtres:</span>
<button type="button" onclick="dashboard.filterSchedules('all')" class="schedule-filter-btn px-3 py-1.5 text-xs rounded-lg bg-purple-600 text-white whitespace-nowrap touch-target" data-filter="all">
Tous
</button>
<button type="button" onclick="dashboard.filterSchedules('active')" class="schedule-filter-btn px-3 py-1.5 text-xs rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600 whitespace-nowrap touch-target" data-filter="active">
Actifs
</button>
<button type="button" onclick="dashboard.filterSchedules('paused')" class="schedule-filter-btn px-3 py-1.5 text-xs rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600 whitespace-nowrap touch-target" data-filter="paused">
En pause
</button>
</div>
<!-- Search - full width on mobile -->
<div class="relative flex-1 sm:max-w-64">
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
<input type="text" id="schedule-search" placeholder="Rechercher..."
class="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm"
oninput="dashboard.searchSchedules(this.value)">
</div>
</div>
</div>
<!-- List View -->
<div id="schedules-list-view" class="glass-card p-4 sm:p-6">
<div id="schedules-list" class="space-y-3">
<!-- Schedules will be populated by JavaScript -->
</div>
<!-- Empty State -->
<div id="schedules-empty" class="hidden text-center py-16">
<div class="w-20 h-20 mx-auto mb-6 bg-purple-600/20 rounded-full flex items-center justify-center">
<i class="fas fa-calendar-plus text-4xl text-purple-400"></i>
</div>
<h3 class="text-xl font-semibold mb-2">Aucun schedule configuré</h3>
<p class="text-gray-400 mb-6 max-w-md mx-auto">
Créez votre premier schedule pour automatiser l'exécution de vos playbooks Ansible.
</p>
<button type="button" onclick="showCreateScheduleModal()" class="btn-primary">
<i class="fas fa-plus mr-2"></i>Créer votre premier schedule
</button>
</div>
</div>
<!-- Calendar View (Hidden by default) -->
<div id="schedules-calendar-view" class="glass-card p-6 hidden">
<div class="flex items-center justify-between mb-6">
<button type="button" onclick="dashboard.prevCalendarMonth()" class="p-2 bg-gray-700 rounded-lg hover:bg-gray-600">
<i class="fas fa-chevron-left"></i>
</button>
<h3 id="schedule-calendar-title" class="text-xl font-semibold">Décembre 2025</h3>
<button type="button" onclick="dashboard.nextCalendarMonth()" class="p-2 bg-gray-700 rounded-lg hover:bg-gray-600">
<i class="fas fa-chevron-right"></i>
</button>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<div class="text-center text-xs text-gray-500 py-2">Lun</div>
<div class="text-center text-xs text-gray-500 py-2">Mar</div>
<div class="text-center text-xs text-gray-500 py-2">Mer</div>
<div class="text-center text-xs text-gray-500 py-2">Jeu</div>
<div class="text-center text-xs text-gray-500 py-2">Ven</div>
<div class="text-center text-xs text-gray-500 py-2">Sam</div>
<div class="text-center text-xs text-gray-500 py-2">Dim</div>
</div>
<div id="schedule-calendar-grid" class="grid grid-cols-7 gap-1">
<!-- Calendar days will be populated by JavaScript -->
</div>
</div>
<!-- Upcoming Executions -->
<div class="glass-card p-6 mt-6">
<h3 class="text-lg font-semibold mb-4">
<i class="fas fa-clock text-blue-400 mr-2"></i>Prochaines exécutions
</h3>
<div id="schedules-upcoming" class="space-y-2">
<!-- Upcoming executions will be populated by JavaScript -->
</div>
</div>
</div>
</div>
</section>
<!-- END PAGE: SCHEDULES -->
<section id="page-alerts" class="page-section">
<div class="pt-20 sm:pt-24 pb-8 sm:pb-16 min-h-screen bg-gradient-to-b from-gray-900 to-black">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<div class="text-center mb-6 sm:mb-12">
<h1 class="text-2xl sm:text-3xl md:text-4xl font-bold mb-2 sm:mb-4 gradient-text">
<i class="fas fa-bell mr-2 sm:mr-3"></i>Centre d'Alertes
</h1>
<p class="text-sm sm:text-base text-gray-400 px-4">Tous les messages reçus (toasts) avec statut lu/non-lu, date et catégorie</p>
</div>
<div class="glass-card p-4 sm:p-6 fade-in">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h3 class="text-lg sm:text-xl font-semibold">Messages</h3>
<div class="flex gap-2 sm:gap-3">
<button type="button" class="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors text-sm touch-target" onclick="dashboard.refreshAlerts()">
<i class="fas fa-sync-alt mr-1 sm:mr-2"></i>
<span class="hidden sm:inline">Rafraîchir</span>
</button>
<button type="button" class="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-purple-600 rounded-lg hover:bg-purple-500 transition-colors text-sm touch-target" onclick="dashboard.markAllAlertsRead()">
<i class="fas fa-check-double mr-1 sm:mr-2"></i>
<span class="hidden sm:inline">Tout marquer lu</span>
</button>
</div>
</div>
<div id="alerts-container" class="space-y-2 max-h-[600px] overflow-y-auto">
</div>
</div>
</div>
</div>
</section>
<!-- ==================== PAGE: DOCKER ==================== -->
<section id="page-docker" class="page-section">
<div class="pt-20 sm:pt-24 pb-8 sm:pb-16 min-h-screen bg-gradient-to-b from-gray-900 to-black">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<!-- Header -->
<div class="text-center mb-6 sm:mb-12">
<h2 class="text-2xl sm:text-3xl font-bold gradient-text fade-in">
<i class="fab fa-docker mr-2"></i>Docker Hosts
</h2>
<p class="text-gray-400 mt-2">Surveillance et gestion des containers Docker</p>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8">
<div class="metric-card">
<div class="text-2xl font-bold text-purple-400" id="docker-stat-hosts">0</div>
<div class="text-gray-400 text-sm">Hosts Docker</div>
</div>
<div class="metric-card metric-card-clickable cursor-pointer hover:border-green-500/50 focus:outline-none focus:ring-2 focus:ring-green-500/50"
onclick="navigateTo('docker-containers')"
onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();navigateTo('docker-containers');}"
tabindex="0"
role="button"
aria-label="Voir tous les containers"
title="Cliquer pour voir tous les containers">
<div class="text-2xl font-bold text-green-400" id="docker-stat-containers">0</div>
<div class="text-gray-400 text-sm flex items-center justify-center gap-1">
Containers
<i class="fas fa-external-link-alt text-xs opacity-50"></i>
</div>
</div>
<div class="metric-card">
<div class="text-2xl font-bold text-blue-400" id="docker-stat-images">0</div>
<div class="text-gray-400 text-sm">Images</div>
</div>
<div class="metric-card">
<div class="text-2xl font-bold text-red-400" id="docker-stat-alerts">0</div>
<div class="text-gray-400 text-sm">Alertes</div>
</div>
</div>
<!-- Actions Bar -->
<div class="flex flex-wrap gap-4 mb-6 items-center justify-between">
<div class="flex gap-2">
<button onclick="dockerSection.collectAll()" class="btn-primary flex items-center gap-2">
<i class="fas fa-sync"></i>
<span class="hidden sm:inline">Collecter Tout</span>
</button>
<button onclick="dockerSection.showAlerts()" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors flex items-center gap-2">
<i class="fas fa-bell"></i>
<span class="hidden sm:inline">Alertes Docker</span>
<span id="docker-alerts-badge" class="hidden px-2 py-0.5 bg-red-600 text-white text-xs rounded-full">0</span>
</button>
</div>
<input type="text" id="docker-search" placeholder="Rechercher un host..."
class="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 w-full sm:w-64">
</div>
<!-- Docker Hosts Grid -->
<div id="docker-hosts-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Docker host cards will be rendered here by JS -->
<div class="glass-card p-6 text-center col-span-full">
<div class="loading-spinner mx-auto mb-4"></div>
<p class="text-gray-400">Chargement des hosts Docker...</p>
</div>
</div>
</div>
</div>
</section>
<!-- END PAGE: DOCKER -->
<!-- Docker Host Detail Modal -->
<div id="docker-detail-modal" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
<div class="glass-card w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
<div class="flex justify-between items-center p-4 border-b border-gray-700">
<h3 id="docker-host-name" class="text-xl font-bold"><i class="fab fa-docker mr-2 text-blue-400"></i>Host Docker</h3>
<button onclick="dockerSection.closeModal()" class="p-2 hover:bg-gray-700 rounded-lg transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Tabs -->
<div class="flex border-b border-gray-700">
<button class="docker-tab active px-4 py-3 text-sm font-medium" data-tab="containers">
<i class="fas fa-box mr-2"></i>Containers
</button>
<button class="docker-tab px-4 py-3 text-sm font-medium text-gray-400" data-tab="images">
<i class="fas fa-layer-group mr-2"></i>Images
</button>
<button class="docker-tab px-4 py-3 text-sm font-medium text-gray-400" data-tab="volumes">
<i class="fas fa-database mr-2"></i>Volumes
</button>
<button class="docker-tab px-4 py-3 text-sm font-medium text-gray-400" data-tab="host-alerts">
<i class="fas fa-exclamation-triangle mr-2"></i>Alertes
</button>
</div>
<!-- Tab Content -->
<div class="flex-1 overflow-auto p-4">
<div id="docker-tab-containers" class="docker-tab-content">
<div id="docker-containers-list" class="space-y-2">
<!-- Containers will be rendered here -->
</div>
</div>
<div id="docker-tab-images" class="docker-tab-content hidden">
<div id="docker-images-list" class="space-y-2">
<!-- Images will be rendered here -->
</div>
</div>
<div id="docker-tab-volumes" class="docker-tab-content hidden">
<div id="docker-volumes-list" class="space-y-2">
<!-- Volumes will be rendered here -->
</div>
</div>
<div id="docker-tab-host-alerts" class="docker-tab-content hidden">
<div id="docker-host-alerts-list" class="space-y-2">
<!-- Host alerts will be rendered here -->
</div>
</div>
</div>
</div>
</div>
<!-- Container Logs Modal -->
<div id="docker-logs-modal" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
<div class="glass-card w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
<div class="flex justify-between items-center p-4 border-b border-gray-700">
<h3 id="docker-logs-title" class="text-xl font-bold"><i class="fas fa-file-alt mr-2 text-green-400"></i>Container Logs</h3>
<button onclick="dockerSection.closeLogsModal()" class="p-2 hover:bg-gray-700 rounded-lg transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
<div class="flex-1 overflow-auto p-4">
<pre id="docker-logs-content" class="bg-black/50 p-4 rounded-lg text-sm font-mono text-gray-300 whitespace-pre-wrap overflow-x-auto"></pre>
</div>
</div>
</div>
<!-- ==================== PAGE: DOCKER CONTAINERS (ALL HOSTS) ==================== -->
<section id="page-docker-containers" class="page-section">
<div class="pt-20 sm:pt-24 pb-8 sm:pb-16 min-h-screen bg-gradient-to-b from-gray-900 to-black">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
<div>
<div class="flex items-center gap-3">
<button onclick="navigateTo('docker')" class="p-2 hover:bg-gray-800 rounded-lg transition-colors" title="Retour aux Docker Hosts">
<i class="fas fa-arrow-left text-gray-400"></i>
</button>
<h2 class="text-2xl sm:text-3xl font-bold gradient-text">
<i class="fas fa-box mr-2"></i>Containers
</h2>
</div>
<p class="text-gray-400 mt-1 ml-11">Tous les containers de vos Docker hosts</p>
</div>
<div class="flex items-center gap-2 text-sm text-gray-500">
<span id="containers-last-update"></span>
<button onclick="containersPage.refresh()" class="p-2 hover:bg-gray-800 rounded-lg transition-colors" title="Actualiser">
<i class="fas fa-sync" id="containers-refresh-icon"></i>
</button>
</div>
</div>
<!-- Stats Summary -->
<div class="grid grid-cols-2 sm:grid-cols-5 gap-3 mb-6">
<div class="bg-gray-800/50 rounded-lg p-3 text-center">
<div class="text-xl font-bold text-white" id="containers-total">0</div>
<div class="text-xs text-gray-400">Total</div>
</div>
<div class="bg-gray-800/50 rounded-lg p-3 text-center">
<div class="text-xl font-bold text-green-400" id="containers-running">0</div>
<div class="text-xs text-gray-400">Running</div>
</div>
<div class="bg-gray-800/50 rounded-lg p-3 text-center">
<div class="text-xl font-bold text-red-400" id="containers-stopped">0</div>
<div class="text-xs text-gray-400">Stopped</div>
</div>
<div class="bg-gray-800/50 rounded-lg p-3 text-center">
<div class="text-xl font-bold text-yellow-400" id="containers-paused">0</div>
<div class="text-xs text-gray-400">Paused</div>
</div>
<div class="bg-gray-800/50 rounded-lg p-3 text-center">
<div class="text-xl font-bold text-purple-400" id="containers-hosts-count">0</div>
<div class="text-xs text-gray-400">Hosts</div>
</div>
</div>
<!-- Toolbar -->
<div class="glass-card p-4 mb-4 sticky top-16 z-30">
<div class="flex flex-col lg:flex-row gap-4">
<!-- Search -->
<div class="flex-1 relative">
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
<input type="text" id="containers-search"
placeholder="Rechercher... (/ pour focus, ex: host:dev status:running)"
class="w-full pl-10 pr-10 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-purple-500"
aria-label="Rechercher des containers">
<button id="containers-search-clear" class="hidden absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Filters -->
<div class="flex flex-wrap gap-2">
<!-- Status Filter -->
<select id="containers-filter-status" class="px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-purple-500" aria-label="Filtrer par statut">
<option value="">Tous les statuts</option>
<option value="running">Running</option>
<option value="exited">Exited</option>
<option value="paused">Paused</option>
<option value="restarting">Restarting</option>
<option value="created">Created</option>
<option value="dead">Dead</option>
</select>
<!-- Host Filter -->
<select id="containers-filter-host" class="px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-purple-500" aria-label="Filtrer par host">
<option value="">Tous les hosts</option>
</select>
<!-- Health Filter -->
<select id="containers-filter-health" class="px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-purple-500" aria-label="Filtrer par santé">
<option value="">Toute santé</option>
<option value="healthy">Healthy</option>
<option value="unhealthy">Unhealthy</option>
<option value="starting">Starting</option>
<option value="none">No healthcheck</option>
</select>
<!-- Sort -->
<select id="containers-sort" class="px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-purple-500" aria-label="Trier par">
<option value="name-asc">Nom A-Z</option>
<option value="name-desc">Nom Z-A</option>
<option value="host-asc">Host A-Z</option>
<option value="status-asc">Statut</option>
<option value="updated-desc">Récent</option>
</select>
</div>
<!-- View Toggle -->
<div class="flex items-center gap-2">
<button id="containers-view-comfortable" class="p-2 bg-purple-600 rounded-lg transition-colors" title="Vue confortable" aria-pressed="true">
<i class="fas fa-th-large"></i>
</button>
<button id="containers-view-compact" class="p-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors" title="Vue compacte" aria-pressed="false">
<i class="fas fa-list"></i>
</button>
<button id="containers-view-grouped" class="p-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors" title="Grouper par host" aria-pressed="false">
<i class="fas fa-layer-group"></i>
</button>
</div>
</div>
<!-- Active Filters Tags -->
<div id="containers-active-filters" class="hidden flex flex-wrap gap-2 mt-3 pt-3 border-t border-gray-700">
</div>
</div>
<!-- Bulk Actions Bar (hidden by default) -->
<div id="containers-bulk-actions" class="hidden glass-card p-3 mb-4 flex items-center justify-between">
<div class="flex items-center gap-3">
<input type="checkbox" id="containers-select-all" class="w-4 h-4 rounded" aria-label="Sélectionner tout">
<span class="text-sm text-gray-400"><span id="containers-selected-count">0</span> sélectionné(s)</span>
</div>
<div class="flex gap-2">
<button onclick="containersPage.bulkAction('start')" class="px-3 py-1.5 bg-green-600/20 text-green-400 hover:bg-green-600/30 rounded text-sm transition-colors">
<i class="fas fa-play mr-1"></i>Start
</button>
<button onclick="containersPage.bulkAction('stop')" class="px-3 py-1.5 bg-red-600/20 text-red-400 hover:bg-red-600/30 rounded text-sm transition-colors">
<i class="fas fa-stop mr-1"></i>Stop
</button>
<button onclick="containersPage.bulkAction('restart')" class="px-3 py-1.5 bg-yellow-600/20 text-yellow-400 hover:bg-yellow-600/30 rounded text-sm transition-colors">
<i class="fas fa-redo mr-1"></i>Restart
</button>
</div>
</div>
<!-- Containers List -->
<div id="containers-list" class="space-y-2" role="list" aria-label="Liste des containers">
<!-- Loading state -->
<div class="glass-card p-8 text-center">
<div class="loading-spinner mx-auto mb-4"></div>
<p class="text-gray-400">Chargement des containers...</p>
</div>
</div>
<!-- Empty State -->
<div id="containers-empty" class="hidden glass-card p-8 text-center">
<i class="fas fa-box-open text-4xl text-gray-600 mb-4"></i>
<p class="text-gray-400 mb-2">Aucun container trouvé</p>
<p class="text-gray-500 text-sm">Modifiez vos filtres ou collectez des données Docker</p>
</div>
<!-- Error State -->
<div id="containers-error" class="hidden glass-card p-8 text-center border border-red-500/30">
<i class="fas fa-exclamation-triangle text-4xl text-red-400 mb-4"></i>
<p class="text-red-400 mb-2">Erreur lors du chargement</p>
<p class="text-gray-500 text-sm mb-4" id="containers-error-message"></p>
<button onclick="containersPage.refresh()" class="px-4 py-2 bg-red-600/20 text-red-400 hover:bg-red-600/30 rounded-lg transition-colors">
<i class="fas fa-redo mr-2"></i>Réessayer
</button>
</div>
<!-- Pagination -->
<div id="containers-pagination" class="hidden flex items-center justify-between mt-6">
<div class="text-sm text-gray-500">
Affichage <span id="containers-showing-start">1</span>-<span id="containers-showing-end">50</span> sur <span id="containers-showing-total">0</span>
</div>
<div class="flex items-center gap-2">
<select id="containers-per-page" class="px-2 py-1 bg-gray-800 border border-gray-700 rounded text-sm">
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
<option value="200">200</option>
</select>
<span class="text-sm text-gray-500">par page</span>
</div>
</div>
</div>
</div>
</section>
<!-- END PAGE: DOCKER CONTAINERS -->
<!-- Container Detail Drawer -->
<div id="container-drawer" class="fixed inset-y-0 right-0 w-full sm:w-[500px] lg:w-[600px] bg-gray-900 border-l border-gray-700 transform translate-x-full transition-transform duration-300 z-50 flex flex-col" role="dialog" aria-labelledby="drawer-title" aria-modal="true">
<div class="flex items-center justify-between p-4 border-b border-gray-700">
<h3 id="drawer-title" class="text-lg font-semibold truncate flex items-center gap-2">
<span id="drawer-container-state" class="w-3 h-3 rounded-full bg-gray-500"></span>
<span id="drawer-container-name">Container</span>
</h3>
<button onclick="containersPage.closeDrawer()" class="p-2 hover:bg-gray-800 rounded-lg transition-colors" aria-label="Fermer">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Drawer Tabs -->
<div class="flex border-b border-gray-700">
<button class="drawer-tab active px-4 py-3 text-sm font-medium" data-tab="overview">
<i class="fas fa-info-circle mr-2"></i>Overview
</button>
<button class="drawer-tab px-4 py-3 text-sm font-medium text-gray-400" data-tab="logs">
<i class="fas fa-file-alt mr-2"></i>Logs
</button>
<button class="drawer-tab px-4 py-3 text-sm font-medium text-gray-400" data-tab="inspect">
<i class="fas fa-code mr-2"></i>Inspect
</button>
</div>
<!-- Drawer Content -->
<div class="flex-1 overflow-auto">
<!-- Overview Tab -->
<div id="drawer-tab-overview" class="drawer-tab-content p-4 space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="bg-gray-800/50 p-3 rounded-lg">
<div class="text-xs text-gray-500 mb-1">Host</div>
<div class="font-medium" id="drawer-host-name"></div>
<div class="text-xs text-gray-500" id="drawer-host-ip"></div>
</div>
<div class="bg-gray-800/50 p-3 rounded-lg">
<div class="text-xs text-gray-500 mb-1">Status</div>
<div class="font-medium" id="drawer-status"></div>
<div class="text-xs text-gray-500" id="drawer-health"></div>
</div>
</div>
<div class="bg-gray-800/50 p-3 rounded-lg">
<div class="text-xs text-gray-500 mb-1">Image</div>
<div class="font-mono text-sm break-all" id="drawer-image"></div>
</div>
<div class="bg-gray-800/50 p-3 rounded-lg">
<div class="text-xs text-gray-500 mb-1">Ports</div>
<div id="drawer-ports" class="flex flex-wrap gap-2"></div>
</div>
<div class="bg-gray-800/50 p-3 rounded-lg">
<div class="text-xs text-gray-500 mb-1">Labels</div>
<div id="drawer-labels" class="flex flex-wrap gap-2 text-xs"></div>
</div>
<div class="bg-gray-800/50 p-3 rounded-lg">
<div class="text-xs text-gray-500 mb-1">Container ID</div>
<div class="font-mono text-sm break-all" id="drawer-container-id"></div>
</div>
<!-- Actions -->
<div class="flex flex-wrap gap-2 pt-4 border-t border-gray-700">
<button onclick="containersPage.drawerAction('start')" id="drawer-btn-start" class="flex-1 px-4 py-2 bg-green-600/20 text-green-400 hover:bg-green-600/30 rounded-lg transition-colors">
<i class="fas fa-play mr-2"></i>Start
</button>
<button onclick="containersPage.drawerAction('stop')" id="drawer-btn-stop" class="flex-1 px-4 py-2 bg-red-600/20 text-red-400 hover:bg-red-600/30 rounded-lg transition-colors">
<i class="fas fa-stop mr-2"></i>Stop
</button>
<button onclick="containersPage.drawerAction('restart')" class="flex-1 px-4 py-2 bg-yellow-600/20 text-yellow-400 hover:bg-yellow-600/30 rounded-lg transition-colors">
<i class="fas fa-redo mr-2"></i>Restart
</button>
<button onclick="containersPage.drawerAction('redeploy')" class="px-4 py-2 bg-purple-600/20 text-purple-400 hover:bg-purple-600/30 rounded-lg transition-colors" title="Pull latest image and recreate">
<i class="fas fa-rocket"></i>
</button>
</div>
</div>
<!-- Logs Tab -->
<div id="drawer-tab-logs" class="drawer-tab-content hidden p-4">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<select id="drawer-logs-tail" class="px-2 py-1 bg-gray-800 border border-gray-700 rounded text-sm">
<option value="100">100 lignes</option>
<option value="200" selected>200 lignes</option>
<option value="500">500 lignes</option>
<option value="1000">1000 lignes</option>
</select>
<label class="flex items-center gap-2 text-sm text-gray-400">
<input type="checkbox" id="drawer-logs-timestamps" class="rounded">
Timestamps
</label>
</div>
<button onclick="containersPage.loadLogs()" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded text-sm transition-colors">
<i class="fas fa-sync mr-1"></i>Actualiser
</button>
</div>
<pre id="drawer-logs-content" class="bg-black/50 p-4 rounded-lg text-xs font-mono text-gray-300 whitespace-pre-wrap overflow-x-auto max-h-[60vh]">Chargement...</pre>
</div>
<!-- Inspect Tab -->
<div id="drawer-tab-inspect" class="drawer-tab-content hidden p-4">
<div class="flex justify-end mb-3">
<button onclick="containersPage.copyInspect()" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded text-sm transition-colors">
<i class="fas fa-copy mr-1"></i>Copier JSON
</button>
</div>
<pre id="drawer-inspect-content" class="bg-black/50 p-4 rounded-lg text-xs font-mono text-gray-300 whitespace-pre-wrap overflow-x-auto max-h-[60vh]">Chargement...</pre>
</div>
</div>
</div>
<!-- Drawer Backdrop -->
<div id="container-drawer-backdrop" class="fixed inset-0 bg-black/50 z-40 hidden" onclick="containersPage.closeDrawer()"></div>
<section id="page-configuration" class="page-section">
<div class="pt-20 sm:pt-24 pb-8 sm:pb-16 min-h-screen bg-gradient-to-b from-gray-900 to-black">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<div class="text-center mb-6 sm:mb-12">
<h1 class="text-2xl sm:text-3xl md:text-4xl font-bold mb-2 sm:mb-4 gradient-text">
<i class="fas fa-sliders-h mr-2 sm:mr-3"></i>Configuration
</h1>
<p class="text-sm sm:text-base text-gray-400 px-4">Paramètres et outils d'administration</p>
</div>
<div class="glass-card p-4 sm:p-6 fade-in">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h3 class="text-lg sm:text-xl font-semibold">Outils de base</h3>
<div class="flex gap-2 sm:gap-3">
<button type="button" class="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-purple-600 rounded-lg hover:bg-purple-500 transition-colors text-sm touch-target" onclick="dashboard.installBaseToolsAllHosts()">
<i class="fas fa-tools mr-1 sm:mr-2"></i>
<span class="hidden sm:inline">Installer sur tous les hôtes</span>
<span class="sm:hidden">Installer</span>
</button>
</div>
</div>
<div class="p-3 bg-gray-800/40 border border-gray-700 rounded-lg text-sm text-gray-300">
<div class="flex gap-2">
<i class="fas fa-info-circle text-blue-400 mt-0.5"></i>
<div>
<p class="font-medium">Ce bouton exécute un builtin playbook d'installation</p>
<p class="text-gray-400">Installe notamment: coreutils, util-linux (lsblk), gawk/grep, python3, iproute2/procps, et optionnels: lvm2, lm-sensors, zfsutils-linux.</p>
</div>
</div>
</div>
</div>
<div class="glass-card p-4 sm:p-6 mt-6 fade-in">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h3 class="text-lg sm:text-xl font-semibold">Collecte des métriques</h3>
<div class="flex gap-2 sm:gap-3">
<button type="button" id="metrics-collection-save" class="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-purple-600 rounded-lg hover:bg-purple-500 transition-colors text-sm touch-target">
<i class="fas fa-save mr-1 sm:mr-2"></i>
<span class="hidden sm:inline">Sauvegarder</span>
<span class="sm:hidden">OK</span>
</button>
</div>
</div>
<div class="space-y-3">
<div>
<label class="block text-sm text-gray-400 mb-1">Période de collecte</label>
<select id="metrics-collection-interval" class="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg">
<option value="off">off</option>
<option value="5min">5 min</option>
<option value="15min">15 min</option>
<option value="30min">30 min</option>
<option value="1h">1 h</option>
<option value="6h">6 h</option>
<option value="12h">12 h</option>
<option value="24h">24 h</option>
</select>
<p class="text-xs text-gray-500 mt-1" id="metrics-collection-current">Chargement…</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ==================== PAGE: LOGS ==================== -->
<section id="page-logs" class="page-section">
<div id="logs" class="pt-20 sm:pt-24 pb-8 sm:pb-16 min-h-screen bg-gradient-to-b from-black to-gray-900">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<div class="text-center mb-6 sm:mb-12">
<h1 class="text-2xl sm:text-3xl md:text-4xl font-bold mb-2 sm:mb-4 gradient-text">
<i class="fas fa-file-alt mr-2 sm:mr-3"></i>Logs Système
</h1>
<p class="text-sm sm:text-base text-gray-400 px-4">Consultez l'historique des opérations et événements système</p>
</div>
<div class="glass-card p-4 sm:p-6 fade-in">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h3 class="text-lg sm:text-xl font-semibold">Logs Récentes</h3>
<div class="flex flex-col sm:flex-row gap-2 sm:gap-3">
<div class="flex-1 sm:flex-none">
<input id="logs-search" type="search" placeholder="Rechercher…" class="w-full sm:w-64 px-3 sm:px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm" />
</div>
<div class="flex gap-2">
<button type="button" class="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg hover:bg-gray-700 transition-colors text-sm touch-target" onclick="dashboard.setLogsView('server')" title="Console" aria-label="Console">
<i class="fas fa-terminal"></i>
</button>
<button type="button" class="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg hover:bg-gray-700 transition-colors text-sm touch-target" onclick="dashboard.setLogsView('db')" title="BD" aria-label="BD">
<i class="fas fa-database"></i>
</button>
</div>
<div class="flex gap-2 sm:gap-3">
<button type="button" class="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors text-sm touch-target" onclick="dashboard.refreshSystemLogs()" title="Mettre à jour" aria-label="Mettre à jour">
<i class="fas fa-sync"></i>
</button>
<button type="button" class="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors text-sm touch-target" onclick="dashboard.clearLogs()" title="Effacer" aria-label="Effacer">
<i class="fas fa-trash"></i>
</button>
<button type="button" class="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors text-sm touch-target" onclick="dashboard.exportLogs()" title="Exporter" aria-label="Exporter">
<i class="fas fa-download"></i>
</button>
</div>
</div>
</div>
<div id="logs-container" class="max-h-[400px] sm:max-h-[600px] overflow-y-auto text-xs sm:text-sm">
<!-- 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-20 sm:pt-24 pb-8 sm:pb-16 min-h-screen bg-gradient-to-b from-gray-900 to-black">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<!-- Header -->
<div class="text-center mb-6 sm:mb-12">
<h1 class="text-2xl sm:text-3xl md:text-4xl font-bold mb-2 sm:mb-4 gradient-text">
🚀 Guide d'Utilisation
</h1>
<p class="text-sm sm:text-base text-gray-400 max-w-2xl mx-auto px-4">
Bienvenue dans le guide officiel de votre <strong class="text-purple-400">Homelab Automation Dashboard</strong> !
Découvrez comment gérer et automatiser efficacement votre infrastructure grâce à cette solution puissante et centralisée.
</p>
<div class="mt-4 flex flex-col sm:flex-row gap-2 sm:gap-3 justify-center px-4">
<button type="button" class="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg hover:bg-gray-700 transition-colors text-sm touch-target" onclick="dashboard.downloadHelpDocumentation('md')">
<i class="fas fa-file-alt mr-2"></i>Télécharger (.md)
</button>
<button type="button" class="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg hover:bg-gray-700 transition-colors text-sm touch-target" onclick="dashboard.downloadHelpDocumentation('pdf')">
<i class="fas fa-file-pdf mr-2"></i>Télécharger (.pdf)
</button>
</div>
</div>
<!-- Layout avec Table des Matières -->
<div class="flex gap-8">
<!-- Table des Matières (sidebar gauche) - Chargée dynamiquement -->
<aside class="hidden lg:block w-64 flex-shrink-0">
<div class="help-toc glass-card p-4 sticky top-24">
<div class="help-toc-title">Table des Matières</div>
<nav id="help-toc-nav">
<!-- TOC chargée dynamiquement depuis help.md -->
<div class="text-gray-500 text-sm py-2">Chargement...</div>
</nav>
</div>
</aside>
<!-- Contenu Principal - Chargé dynamiquement depuis help.md -->
<div id="help-dynamic-content" class="flex-1 help-main-content">
<!-- Placeholder pendant le chargement -->
<div class="glass-card p-8 mb-8 text-center">
<div class="loading-spinner mx-auto mb-4"></div>
<p class="text-gray-400">Chargement de la documentation...</p>
</div>
</div>
</div>
</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 type="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>
<script>
// Fallback global helper in case main.js didn't expose it
if (typeof window.showCreateScheduleModal === 'undefined') {
window.showCreateScheduleModal = function(prefilledPlaybook = null) {
if (window.dashboard && typeof dashboard.showCreateScheduleModal === 'function') {
dashboard.showCreateScheduleModal(prefilledPlaybook);
}
};
}
</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');
});
// Charger dynamiquement le contenu d'aide si nécessaire
if (pageName === 'help') {
loadHelpContent();
}
}
}
// Chargement dynamique du contenu d'aide depuis help.md
let helpContentLoaded = false;
async function loadHelpContent() {
if (helpContentLoaded) return;
const contentContainer = document.getElementById('help-dynamic-content');
const tocNav = document.getElementById('help-toc-nav');
if (!contentContainer) return;
try {
const response = await fetch('/api/help/content', {
headers: window.dashboard ? window.dashboard.getAuthHeaders() : {}
});
if (!response.ok) {
throw new Error('Erreur de chargement');
}
const data = await response.json();
// Injecter le contenu
contentContainer.innerHTML = data.content;
// Injecter la TOC
if (tocNav && data.toc) {
tocNav.innerHTML = data.toc;
}
// Marquer comme chargé
helpContentLoaded = true;
// Réinitialiser les animations fade-in
contentContainer.querySelectorAll('.fade-in, .glass-card').forEach(el => {
el.classList.add('visible');
});
} catch (error) {
console.error('Erreur chargement aide:', error);
contentContainer.innerHTML = `
<div class="glass-card p-8 mb-8 text-center">
<i class="fas fa-exclamation-triangle text-4xl text-yellow-400 mb-4"></i>
<p class="text-gray-400 mb-4">Impossible de charger la documentation.</p>
<button onclick="helpContentLoaded = false; loadHelpContent();" class="px-4 py-2 bg-purple-600 rounded-lg hover:bg-purple-500 transition-colors">
<i class="fas fa-redo mr-2"></i>Réessayer
</button>
</div>
`;
}
}
// 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'] });
}
});
// ===== MOBILE NAVIGATION =====
let mobileNavOpen = false;
function toggleMobileNav() {
mobileNavOpen = !mobileNavOpen;
const overlay = document.getElementById('mobile-nav-overlay');
const sidebar = document.getElementById('mobile-nav-sidebar');
const menuBtn = document.getElementById('mobile-menu-btn');
if (mobileNavOpen) {
overlay.classList.add('active');
sidebar.classList.add('active');
menuBtn.classList.add('active');
document.body.style.overflow = 'hidden';
} else {
closeMobileNav();
}
}
function closeMobileNav() {
mobileNavOpen = false;
const overlay = document.getElementById('mobile-nav-overlay');
const sidebar = document.getElementById('mobile-nav-sidebar');
const menuBtn = document.getElementById('mobile-menu-btn');
overlay.classList.remove('active');
sidebar.classList.remove('active');
menuBtn?.classList.remove('active');
document.body.style.overflow = '';
}
function mobileNavigateTo(pageName) {
closeMobileNav();
navigateTo(pageName);
updateMobileNavLinks(pageName);
}
function updateMobileNavLinks(pageName) {
// Update mobile nav active state
document.querySelectorAll('.mobile-nav-link').forEach(link => {
link.classList.remove('active');
if (link.dataset.page === pageName) {
link.classList.add('active');
}
});
}
// Extended navigateTo to also update mobile nav
const originalNavigateTo = navigateTo;
navigateTo = function(pageName) {
originalNavigateTo(pageName);
updateMobileNavLinks(pageName);
};
// Close mobile nav on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && mobileNavOpen) {
closeMobileNav();
}
});
// Close mobile nav on window resize to desktop
window.addEventListener('resize', function() {
if (window.innerWidth >= 768 && mobileNavOpen) {
closeMobileNav();
}
});
// ===== THEME TOGGLE (with mobile support) =====
function toggleTheme() {
const body = document.body;
const isLight = body.classList.toggle('light-theme');
// Update all theme toggle icons
const themeIcons = document.querySelectorAll('#theme-toggle i, #theme-toggle-mobile i, #mobile-theme-icon');
themeIcons.forEach(icon => {
icon.className = isLight ? 'fas fa-sun text-yellow-400' : 'fas fa-moon text-gray-300';
});
// Update mobile theme label
const mobileLabel = document.getElementById('mobile-theme-label');
if (mobileLabel) {
mobileLabel.textContent = isLight ? 'Thème clair' : 'Thème sombre';
}
// Save preference
localStorage.setItem('theme', isLight ? 'light' : 'dark');
}
// Initialize theme from localStorage
document.addEventListener('DOMContentLoaded', function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
document.body.classList.add('light-theme');
const themeIcons = document.querySelectorAll('#theme-toggle i, #theme-toggle-mobile i, #mobile-theme-icon');
themeIcons.forEach(icon => {
icon.className = 'fas fa-sun text-yellow-400';
});
const mobileLabel = document.getElementById('mobile-theme-label');
if (mobileLabel) {
mobileLabel.textContent = 'Thème clair';
}
}
// Initialize mobile nav links state
updateMobileNavLinks(currentPage);
});
// ===== KEBAB MENU HANDLER =====
function toggleKebabMenu(element, event) {
event.stopPropagation();
const menu = element.closest('.kebab-menu');
const wasOpen = menu.classList.contains('open');
// Close all other kebab menus
document.querySelectorAll('.kebab-menu.open').forEach(m => {
m.classList.remove('open');
});
// Toggle this one
if (!wasOpen) {
menu.classList.add('open');
}
}
// Close kebab menus when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.kebab-menu')) {
document.querySelectorAll('.kebab-menu.open').forEach(m => {
m.classList.remove('open');
});
}
});
// ===== TOUCH INTERACTIONS =====
let touchStartX = 0;
let touchEndX = 0;
// Swipe to open mobile nav
document.addEventListener('touchstart', function(e) {
touchStartX = e.changedTouches[0].screenX;
}, { passive: true });
document.addEventListener('touchend', function(e) {
touchEndX = e.changedTouches[0].screenX;
handleSwipe();
}, { passive: true });
function handleSwipe() {
const swipeThreshold = 80;
const swipeDistance = touchEndX - touchStartX;
// Swipe right from left edge to open nav
if (touchStartX < 30 && swipeDistance > swipeThreshold && !mobileNavOpen) {
toggleMobileNav();
}
// Swipe left to close nav
if (mobileNavOpen && swipeDistance < -swipeThreshold) {
closeMobileNav();
}
}
// ===== VIEWPORT HEIGHT FIX FOR MOBILE =====
function setViewportHeight() {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
setViewportHeight();
window.addEventListener('resize', setViewportHeight);
// ===== MOBILE-OPTIMIZED ANIMATIONS =====
function isMobile() {
return window.innerWidth < 768;
}
// Reduce animation on mobile for better performance
if (isMobile()) {
// Disable float animation on mobile
document.querySelectorAll('.animate-float').forEach(el => {
el.style.animation = 'none';
});
}
// ===== SCROLL PREVENTION WHEN MODAL OPEN =====
const originalShowModal = window.showModal || function() {};
window.showModal = function() {
if (isMobile()) {
document.body.style.overflow = 'hidden';
}
originalShowModal.apply(this, arguments);
};
// Cleanup modal scroll lock
document.addEventListener('click', function(e) {
const modal = document.getElementById('modal');
if (modal && e.target === modal) {
document.body.style.overflow = '';
}
});
// ===== MOBILE DEBUG HELPER =====
// Logs all click events to help diagnose mobile issues
(function() {
let debugMode = true; // Set to false to disable debug logs
if (debugMode) {
document.addEventListener('click', function(e) {
console.log('[DEBUG Click]', {
target: e.target,
tagName: e.target.tagName,
className: e.target.className,
id: e.target.id,
onclick: e.target.onclick ? 'yes' : 'no',
closestButton: e.target.closest('button'),
closestGroup: e.target.closest('.group'),
x: e.clientX,
y: e.clientY
});
}, true);
// Log touch events
document.addEventListener('touchstart', function(e) {
console.log('[DEBUG TouchStart]', e.target.tagName, e.target.className);
}, { passive: true, capture: true });
document.addEventListener('touchend', function(e) {
console.log('[DEBUG TouchEnd]', e.target.tagName, e.target.className);
}, { passive: true, capture: true });
}
})();
// ===== FALLBACK ONCLICK HANDLER =====
// Ensures onclick attributes work on mobile via event delegation
(function() {
document.addEventListener('click', function(e) {
// Find the closest element with an onclick attribute
const clickable = e.target.closest('[onclick]');
if (clickable && !e.defaultPrevented) {
const onclickAttr = clickable.getAttribute('onclick');
if (onclickAttr) {
console.log('[Fallback] Executing onclick:', onclickAttr);
try {
// Execute the onclick handler if it wasn't already triggered
// This is a fallback - normally the browser handles this
} catch (err) {
console.error('[Fallback] onclick error:', err);
}
}
}
}, false); // Use bubble phase to not interfere with capture handlers
})();
// ===== DROPDOWN HANDLER (MOBILE & DESKTOP) =====
// Uses click-based dropdowns on all devices for consistency
// EVENT DELEGATION handles dynamically created elements
(function() {
console.log('[Dropdown] Handler initialized');
// Check if device is mobile or touch
const isTouchDevice = () => 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const shouldUseClickDropdowns = () => isTouchDevice() || isMobile() || window.innerWidth < 769;
// Event delegation for dropdown triggers
document.addEventListener('click', function(e) {
// Find if we clicked on a dropdown trigger (.group > button)
const trigger = e.target.closest('.group > button, .group > [role="button"]');
if (trigger) {
const parent = trigger.closest('.group');
const dropdown = parent.querySelector('.absolute, [class*="absolute"]');
// Only use click behavior on mobile/touch or small screens
if (shouldUseClickDropdowns()) {
e.preventDefault();
e.stopPropagation();
const wasOpen = parent.classList.contains('dropdown-open');
console.log('[Dropdown] Trigger clicked, wasOpen:', wasOpen, 'element:', trigger);
// Close all other dropdowns first
document.querySelectorAll('.group.dropdown-open').forEach(g => {
g.classList.remove('dropdown-open');
});
// Toggle this dropdown
if (!wasOpen) {
parent.classList.add('dropdown-open');
console.log('[Dropdown] Opened:', parent);
}
return;
}
}
// Handle clicks on dropdown menu items - allow action then close
const dropdownItem = e.target.closest('.group .absolute button, .group .absolute a, .group [class*="absolute"] button');
if (dropdownItem && shouldUseClickDropdowns()) {
console.log('[Dropdown] Item clicked:', dropdownItem);
// Let the click action happen, then close dropdown
setTimeout(() => {
document.querySelectorAll('.group.dropdown-open').forEach(g => {
g.classList.remove('dropdown-open');
});
}, 150);
return;
}
// Close all dropdowns when clicking outside any .group
if (!e.target.closest('.group')) {
const openDropdowns = document.querySelectorAll('.group.dropdown-open');
if (openDropdowns.length > 0) {
console.log('[Dropdown] Closing all dropdowns (clicked outside)');
openDropdowns.forEach(g => g.classList.remove('dropdown-open'));
}
}
}, true); // Use capture phase for priority
// Also listen to touchstart for better mobile responsiveness
document.addEventListener('touchstart', function(e) {
const trigger = e.target.closest('.group > button');
if (trigger && shouldUseClickDropdowns()) {
// Mark that we're handling this touch
trigger.dataset.touchStarted = 'true';
}
}, { passive: true });
// Close dropdowns on scroll
let scrollTimeout;
document.addEventListener('scroll', function() {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
document.querySelectorAll('.group.dropdown-open').forEach(g => {
g.classList.remove('dropdown-open');
});
}, 100);
}, { passive: true });
// Close dropdowns on resize
window.addEventListener('resize', function() {
document.querySelectorAll('.group.dropdown-open').forEach(g => {
g.classList.remove('dropdown-open');
});
}, { passive: true });
console.log('[Dropdown] Touch device:', isTouchDevice(), 'Mobile:', isMobile());
})();
</script>
</div><!-- End main-content -->
<!-- Authentication handlers -->
<script>
// Toggle password visibility
function togglePasswordVisibility(inputId, btn) {
const input = document.getElementById(inputId);
const icon = btn.querySelector('i');
if (input.type === 'password') {
input.type = 'text';
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
input.type = 'password';
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
}
// Handle login form submission
async function handleLogin(event) {
event.preventDefault();
const username = document.getElementById('login-username').value;
const password = document.getElementById('login-password').value;
const errorEl = document.getElementById('login-error');
const errorText = document.getElementById('login-error-text');
const submitBtn = document.getElementById('login-submit-btn');
// Reset error state
errorEl.classList.add('hidden');
// Show loading state
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>Connexion...</span>';
try {
const success = await dashboard.login(username, password);
if (!success) {
errorText.textContent = 'Nom d\'utilisateur ou mot de passe incorrect';
errorEl.classList.remove('hidden');
}
} catch (error) {
errorText.textContent = error.message || 'Erreur de connexion';
errorEl.classList.remove('hidden');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-sign-in-alt"></i><span>Se connecter</span>';
}
}
// Handle setup form submission
async function handleSetup(event) {
event.preventDefault();
const username = document.getElementById('setup-username').value;
const password = document.getElementById('setup-password').value;
const passwordConfirm = document.getElementById('setup-password-confirm').value;
const email = document.getElementById('setup-email').value || null;
const displayName = document.getElementById('setup-display-name').value || null;
const errorEl = document.getElementById('setup-error');
const errorText = document.getElementById('setup-error-text');
const submitBtn = document.getElementById('setup-submit-btn');
// Reset error state
errorEl.classList.add('hidden');
// Validate password confirmation
if (password !== passwordConfirm) {
errorText.textContent = 'Les mots de passe ne correspondent pas';
errorEl.classList.remove('hidden');
return;
}
// Show loading state
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>Création...</span>';
try {
const success = await dashboard.setupAdmin(username, password, email, displayName);
if (!success) {
errorText.textContent = 'Erreur lors de la création du compte';
errorEl.classList.remove('hidden');
}
} catch (error) {
errorText.textContent = error.message || 'Erreur de configuration';
errorEl.classList.remove('hidden');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-user-plus"></i><span>Créer le compte</span>';
}
}
// Logout function (exposed globally)
function handleLogout() {
if (dashboard) {
dashboard.logout();
}
}
</script>
<!-- Docker Section JavaScript -->
<script src="/static/docker_section.js"></script>
<!-- Containers Page JavaScript -->
<script src="/static/containers_page.js"></script>
<!-- CodeMirror Editor Bundle -->
<script src="/static/codemirror-editor.js"></script>
</body>
</html>