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
6154 lines
243 KiB
HTML
6154 lines
243 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=Syne:wght@400;500;600;700;800&family=DM+Sans: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">
|
|
|
|
<!-- PWA -->
|
|
<link rel="manifest" href="/manifest.json">
|
|
<meta name="theme-color" content="#0a0f1e">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<link rel="apple-touch-icon" href="/static/icons/icon-192x192.png">
|
|
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
fontFamily: { heading: ['Syne','sans-serif'], body: ['DM Sans','sans-serif'] },
|
|
colors: {
|
|
base: { 900:'#0a0f1e', 800:'#0f1629', 700:'#141c34', 600:'#1a2340' },
|
|
accent: { cyan:'#22d3ee', violet:'#8b5cf6', pink:'#ec4899' }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
:root {
|
|
--primary-bg: #0a0f1e;
|
|
--secondary-bg: #141c34;
|
|
--accent-bg: #1a2340;
|
|
--primary-text: #e2e8f0;
|
|
--secondary-text: #94a3b8;
|
|
--accent-color: #8b5cf6;
|
|
--accent-hover: #a78bfa;
|
|
--success-color: #10b981;
|
|
--warning-color: #f59e0b;
|
|
--error-color: #ef4444;
|
|
--border-color: rgba(255,255,255,0.08);
|
|
|
|
--bg-base: #0a0f1e;
|
|
--bg-card: rgba(255,255,255,0.03);
|
|
--bg-card-hover: rgba(255,255,255,0.06);
|
|
--border: rgba(255,255,255,0.08);
|
|
--border-hover: rgba(255,255,255,0.15);
|
|
|
|
--sidebar-w: 260px;
|
|
--sidebar-collapsed: 68px;
|
|
--header-h: 52px;
|
|
|
|
--glass-blur: blur(20px);
|
|
--glass-bg: rgba(255,255,255,0.03);
|
|
--glass-border: 1px solid rgba(255,255,255,0.08);
|
|
}
|
|
|
|
[data-theme="light"],
|
|
body.light-theme {
|
|
--primary-bg: #f1f5f9;
|
|
--secondary-bg: #ffffff;
|
|
--accent-bg: #e2e8f0;
|
|
--primary-text: #0f172a;
|
|
--secondary-text: #475569;
|
|
--border-color: rgba(0,0,0,0.08);
|
|
|
|
--bg-base: #f1f5f9;
|
|
--bg-card: rgba(255,255,255,0.8);
|
|
--bg-card-hover: rgba(255,255,255,0.95);
|
|
--border: rgba(0,0,0,0.08);
|
|
--border-hover: rgba(0,0,0,0.15);
|
|
|
|
--glass-bg: rgba(255,255,255,0.7);
|
|
--glass-border: 1px solid rgba(0,0,0,0.08);
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html { scroll-behavior: smooth; }
|
|
|
|
body {
|
|
font-family: 'DM Sans', sans-serif;
|
|
background: var(--bg-base);
|
|
color: var(--primary-text);
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
transition: background 0.4s, color 0.4s;
|
|
}
|
|
|
|
h1, h2, h3, h4, h5, h6, .font-heading {
|
|
font-family: 'Syne', sans-serif;
|
|
}
|
|
|
|
.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 ===== */
|
|
[data-theme="light"],
|
|
body.light-theme {
|
|
--primary-bg: #f5f5f5;
|
|
--secondary-bg: #ffffff;
|
|
--accent-bg: #e5e5e5;
|
|
--primary-text: #1a1a1a;
|
|
--secondary-text: #6b7280;
|
|
--border-color: #d1d5db;
|
|
}
|
|
|
|
[data-theme="light"] body,
|
|
body.light-theme {
|
|
background: var(--primary-bg);
|
|
color: var(--primary-text);
|
|
}
|
|
|
|
[data-theme="light"] .hero-section,
|
|
body.light-theme .hero-section {
|
|
background: linear-gradient(135deg, #f5f5f5 0%, #e5e5e5 100%);
|
|
}
|
|
|
|
[data-theme="light"] .glass-card,
|
|
body.light-theme .glass-card {
|
|
background: rgba(255, 255, 255, 0.9);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
[data-theme="light"] .metric-card,
|
|
body.light-theme .metric-card {
|
|
background: rgba(255, 255, 255, 0.8);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
[data-theme="light"] .host-card,
|
|
body.light-theme .host-card {
|
|
background: rgba(255, 255, 255, 0.6);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
[data-theme="light"] .host-card:hover,
|
|
body.light-theme .host-card:hover {
|
|
background: rgba(255, 255, 255, 0.9);
|
|
}
|
|
|
|
[data-theme="light"] nav,
|
|
body.light-theme nav {
|
|
background: rgba(255, 255, 255, 0.9) !important;
|
|
border-color: #e5e5e5 !important;
|
|
}
|
|
|
|
[data-theme="light"] nav a,
|
|
[data-theme="light"] nav h1,
|
|
body.light-theme nav a,
|
|
body.light-theme nav h1 {
|
|
color: #1a1a1a !important;
|
|
}
|
|
|
|
[data-theme="light"] nav a:hover,
|
|
body.light-theme nav a:hover {
|
|
color: #7c3aed !important;
|
|
}
|
|
|
|
[data-theme="light"] .text-white,
|
|
body.light-theme .text-white {
|
|
color: #1a1a1a !important;
|
|
}
|
|
|
|
[data-theme="light"] .text-gray-300,
|
|
[data-theme="light"] .text-gray-400,
|
|
[data-theme="light"] .text-gray-500,
|
|
body.light-theme .text-gray-300,
|
|
body.light-theme .text-gray-400,
|
|
body.light-theme .text-gray-500 {
|
|
color: #4b5563 !important;
|
|
}
|
|
|
|
[data-theme="light"] .text-gray-400,
|
|
body.light-theme .text-gray-400 {
|
|
color: #374151 !important;
|
|
}
|
|
|
|
[data-theme="light"] .text-gray-500,
|
|
body.light-theme .text-gray-500 {
|
|
color: #6b7280 !important;
|
|
}
|
|
|
|
[data-theme="light"] .bg-gray-700,
|
|
[data-theme="light"] .bg-gray-800,
|
|
body.light-theme .bg-gray-700,
|
|
body.light-theme .bg-gray-800 {
|
|
background-color: #e5e5e5 !important;
|
|
}
|
|
|
|
[data-theme="light"] .bg-gray-800\/40,
|
|
body.light-theme .bg-gray-800\/40 {
|
|
background-color: rgba(229, 231, 235, 0.75) !important;
|
|
}
|
|
|
|
[data-theme="light"] .bg-gray-800\/60,
|
|
body.light-theme .bg-gray-800\/60 {
|
|
background-color: rgba(229, 231, 235, 0.9) !important;
|
|
}
|
|
|
|
[data-theme="light"] .border-gray-700\/60,
|
|
body.light-theme .border-gray-700\/60 {
|
|
border-color: #d1d5db !important;
|
|
}
|
|
|
|
[data-theme="light"] .border-red-700\/60,
|
|
body.light-theme .border-red-700\/60 {
|
|
border-color: #fca5a5 !important;
|
|
}
|
|
|
|
[data-theme="light"] .bg-black,
|
|
body.light-theme .bg-black {
|
|
background-color: #f5f5f5 !important;
|
|
}
|
|
|
|
[data-theme="light"] #modal .glass-card,
|
|
body.light-theme #modal .glass-card {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
}
|
|
|
|
[data-theme="light"] .log-entry,
|
|
body.light-theme .log-entry {
|
|
background: rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
[data-theme="light"] footer,
|
|
body.light-theme footer {
|
|
background: #f5f5f5 !important;
|
|
border-color: #e5e5e5 !important;
|
|
}
|
|
|
|
[data-theme="light"] select,
|
|
[data-theme="light"] input,
|
|
body.light-theme select,
|
|
body.light-theme input {
|
|
background-color: #ffffff !important;
|
|
border-color: #d1d5db !important;
|
|
color: #1a1a1a !important;
|
|
}
|
|
|
|
[data-theme="light"] .task-filter-btn:not(.active),
|
|
body.light-theme .task-filter-btn:not(.active) {
|
|
background-color: #e5e5e5 !important;
|
|
color: #1a1a1a !important;
|
|
}
|
|
|
|
[data-theme="light"] pre,
|
|
body.light-theme pre {
|
|
background-color: #1a1a1a !important;
|
|
}
|
|
|
|
/* Thème clair - calendrier de filtrage des tâches */
|
|
[data-theme="light"] .task-calendar,
|
|
body.light-theme .task-calendar {
|
|
background-color: #ffffff !important;
|
|
border-color: #d1d5db !important;
|
|
color: #111827 !important;
|
|
}
|
|
|
|
[data-theme="light"] #task-date-filter-button,
|
|
body.light-theme #task-date-filter-button {
|
|
background-color: #111827 !important;
|
|
border-color: #374151 !important;
|
|
color: #f9fafb !important;
|
|
}
|
|
|
|
[data-theme="light"] #task-date-filter-button:hover,
|
|
body.light-theme #task-date-filter-button:hover {
|
|
background-color: #1f2937 !important;
|
|
}
|
|
|
|
[data-theme="light"] #task-cal-grid button,
|
|
body.light-theme #task-cal-grid button {
|
|
color: #111827 !important;
|
|
}
|
|
|
|
[data-theme="light"] #task-cal-grid button.bg-purple-600,
|
|
body.light-theme #task-cal-grid button.bg-purple-600 {
|
|
color: #f9fafb !important;
|
|
}
|
|
|
|
[data-theme="light"] #task-cal-summary,
|
|
body.light-theme #task-cal-summary {
|
|
color: #4b5563 !important;
|
|
}
|
|
|
|
[data-theme="light"] #task-cal-clear,
|
|
body.light-theme #task-cal-clear {
|
|
background-color: #e5e7eb !important;
|
|
border-color: #d1d5db !important;
|
|
color: #111827 !important;
|
|
}
|
|
|
|
[data-theme="light"] #task-cal-clear:hover,
|
|
body.light-theme #task-cal-clear:hover {
|
|
background-color: #d1d5db !important;
|
|
}
|
|
|
|
[data-theme="light"] #task-cal-apply,
|
|
body.light-theme #task-cal-apply {
|
|
background-color: #7c3aed !important;
|
|
color: #f9fafb !important;
|
|
}
|
|
|
|
/* Viewer de logs Ansible (modal) en thème clair */
|
|
[data-theme="light"] .playbook-execution-viewer,
|
|
body.light-theme .playbook-execution-viewer {
|
|
background-color: #f9fafb;
|
|
}
|
|
|
|
[data-theme="light"] .playbook-execution-viewer .task-hierarchy-section .task-tree,
|
|
body.light-theme .playbook-execution-viewer .task-hierarchy-section .task-tree {
|
|
background-color: #e5e7eb !important;
|
|
border-color: #d1d5db !important;
|
|
}
|
|
|
|
[data-theme="light"] .playbook-execution-viewer .task-tree .host-card-item,
|
|
body.light-theme .playbook-execution-viewer .task-tree .host-card-item {
|
|
background-color: #f3f4f6 !important;
|
|
border-color: #d1d5db !important;
|
|
}
|
|
|
|
[data-theme="light"] .playbook-execution-viewer .task-tree .host-card-item:hover,
|
|
body.light-theme .playbook-execution-viewer .task-tree .host-card-item:hover {
|
|
background-color: #e5e7eb !important;
|
|
}
|
|
|
|
/* Fond sombre semi-transparent utilisé dans le viewer */
|
|
[data-theme="light"] .playbook-execution-viewer .bg-gray-900\/50,
|
|
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) */
|
|
[data-theme="light"] .playbook-execution-viewer .task-item summary .text-gray-200,
|
|
body.light-theme .playbook-execution-viewer .task-item summary .text-gray-200 {
|
|
color: #111827 !important; /* texte presque noir sur fond clair */
|
|
}
|
|
|
|
[data-theme="light"] .playbook-execution-viewer .play-header,
|
|
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 */
|
|
[data-theme="light"] .playbook-execution-viewer .task-tree,
|
|
body.light-theme .playbook-execution-viewer .task-tree {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: #9ca3af #e5e7eb; /* pouce gris sur fond gris clair */
|
|
}
|
|
|
|
[data-theme="light"] .playbook-execution-viewer .task-tree::-webkit-scrollbar,
|
|
body.light-theme .playbook-execution-viewer .task-tree::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
[data-theme="light"] .playbook-execution-viewer .task-tree::-webkit-scrollbar-track,
|
|
body.light-theme .playbook-execution-viewer .task-tree::-webkit-scrollbar-track {
|
|
background: #e5e7eb;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
[data-theme="light"] .playbook-execution-viewer .task-tree::-webkit-scrollbar-thumb,
|
|
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 */
|
|
[data-theme="light"] .playbook-card,
|
|
body.light-theme .playbook-card {
|
|
background: rgba(255, 255, 255, 0.6);
|
|
border-color: #d1d5db;
|
|
}
|
|
|
|
[data-theme="light"] .playbook-card:hover,
|
|
body.light-theme .playbook-card:hover {
|
|
background: rgba(255, 255, 255, 0.9);
|
|
}
|
|
|
|
[data-theme="light"] .playbook-code-editor,
|
|
body.light-theme .playbook-code-editor {
|
|
background: #1e1e1e;
|
|
color: #d4d4d4;
|
|
}
|
|
|
|
[data-theme="light"] .playbook-search,
|
|
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 */
|
|
[data-theme="light"] .schedule-card,
|
|
body.light-theme .schedule-card {
|
|
background: rgba(255, 255, 255, 0.6);
|
|
border-color: #d1d5db;
|
|
}
|
|
|
|
[data-theme="light"] .schedule-card:hover,
|
|
body.light-theme .schedule-card:hover {
|
|
background: rgba(255, 255, 255, 0.9);
|
|
}
|
|
|
|
[data-theme="light"] .schedule-calendar-day,
|
|
body.light-theme .schedule-calendar-day {
|
|
background: rgba(255, 255, 255, 0.6);
|
|
border-color: #e5e7eb;
|
|
}
|
|
|
|
[data-theme="light"] .schedule-calendar-day:hover,
|
|
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 === */
|
|
[data-theme="light"] .mobile-nav-sidebar,
|
|
body.light-theme .mobile-nav-sidebar {
|
|
background: #ffffff;
|
|
border-color: #e5e5e5;
|
|
}
|
|
|
|
[data-theme="light"] .mobile-nav-links button,
|
|
[data-theme="light"] .mobile-nav-links a,
|
|
body.light-theme .mobile-nav-links button,
|
|
body.light-theme .mobile-nav-links a {
|
|
color: #4b5563;
|
|
}
|
|
|
|
[data-theme="light"] .mobile-nav-links button:hover,
|
|
[data-theme="light"] .mobile-nav-links button:active,
|
|
[data-theme="light"] .mobile-nav-links a:hover,
|
|
[data-theme="light"] .mobile-nav-links a:active,
|
|
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;
|
|
}
|
|
|
|
[data-theme="light"] .mobile-nav-links button.active,
|
|
[data-theme="light"] .mobile-nav-links a.active,
|
|
body.light-theme .mobile-nav-links button.active,
|
|
body.light-theme .mobile-nav-links a.active {
|
|
color: #7c3aed;
|
|
}
|
|
|
|
[data-theme="light"] .mobile-action-bar,
|
|
body.light-theme .mobile-action-bar {
|
|
background: #ffffff;
|
|
border-color: #e5e5e5;
|
|
}
|
|
|
|
[data-theme="light"] .kebab-menu-dropdown,
|
|
body.light-theme .kebab-menu-dropdown {
|
|
background: #ffffff;
|
|
border-color: #e5e5e5;
|
|
}
|
|
|
|
[data-theme="light"] .kebab-menu-dropdown button,
|
|
body.light-theme .kebab-menu-dropdown button {
|
|
color: #1f2937;
|
|
}
|
|
|
|
[data-theme="light"] .kebab-menu-dropdown button:hover,
|
|
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;
|
|
}
|
|
}
|
|
|
|
/* =====================================================
|
|
Terminal SSH Drawer Styles
|
|
===================================================== */
|
|
.terminal-drawer {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 1000;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.terminal-drawer.open {
|
|
opacity: 1;
|
|
pointer-events: all;
|
|
}
|
|
|
|
.terminal-drawer-backdrop {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 0;
|
|
}
|
|
|
|
.terminal-drawer-panel {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 50vw;
|
|
min-width: 400px;
|
|
max-width: 900px;
|
|
background: #0f0f1a;
|
|
border-left: 1px solid #374151;
|
|
display: flex;
|
|
flex-direction: column;
|
|
z-index: 1;
|
|
transform: translateX(100%);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.terminal-drawer.open .terminal-drawer-panel {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.terminal-drawer-header {
|
|
padding: 0.75rem 1rem;
|
|
background: #1a1a2e;
|
|
border-bottom: 1px solid #374151;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.75rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.terminal-host-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
min-width: 0;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.terminal-host-name {
|
|
font-weight: 600;
|
|
color: #fff;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.terminal-host-ip {
|
|
color: #9ca3af;
|
|
font-size: 0.875rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.terminal-status-badge {
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 9999px;
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.terminal-status-badge.online {
|
|
background: rgba(34, 197, 94, 0.2);
|
|
color: #4ade80;
|
|
}
|
|
|
|
.terminal-status-badge.connecting {
|
|
background: rgba(251, 191, 36, 0.2);
|
|
color: #fbbf24;
|
|
}
|
|
|
|
.terminal-header-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.terminal-btn {
|
|
padding: 0.5rem 0.75rem;
|
|
border-radius: 0.375rem;
|
|
font-size: 0.875rem;
|
|
border: none;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.terminal-btn-secondary {
|
|
background: #374151;
|
|
color: #e5e7eb;
|
|
}
|
|
|
|
.terminal-btn-secondary:hover {
|
|
background: #4b5563;
|
|
}
|
|
|
|
.terminal-btn-danger {
|
|
background: #dc2626;
|
|
color: #fff;
|
|
}
|
|
|
|
.terminal-btn-danger:hover {
|
|
background: #b91c1c;
|
|
}
|
|
|
|
.terminal-drawer-body {
|
|
flex: 1;
|
|
position: relative;
|
|
overflow: hidden;
|
|
background: #000;
|
|
}
|
|
|
|
.terminal-iframe {
|
|
width: 100%;
|
|
height: 100%;
|
|
border: none;
|
|
background: #000;
|
|
}
|
|
|
|
.terminal-loading {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #0f0f1a;
|
|
}
|
|
|
|
.terminal-loading .spinner {
|
|
width: 48px;
|
|
height: 48px;
|
|
border: 3px solid #374151;
|
|
border-top-color: #3b82f6;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
.terminal-loading .loading-text {
|
|
margin-top: 1rem;
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.terminal-drawer-footer {
|
|
padding: 0.5rem 1rem;
|
|
background: #1a1a2e;
|
|
border-top: 1px solid #374151;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.terminal-timer {
|
|
margin-left: auto;
|
|
font-size: 0.875rem;
|
|
color: #9ca3af;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.terminal-timer.warning {
|
|
color: #fbbf24;
|
|
}
|
|
|
|
.terminal-timer.expired {
|
|
color: #ef4444;
|
|
}
|
|
|
|
/* Terminal History Panel */
|
|
.terminal-history-panel {
|
|
position: absolute;
|
|
top: 57px; /* Matches header height */
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 100;
|
|
background: #1e1e2e;
|
|
border-bottom: 1px solid #374151;
|
|
max-height: 0;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
opacity: 0;
|
|
transition: max-height 0.3s ease, opacity 0.2s ease, transform 0.2s ease;
|
|
box-shadow: 0 5px 25px rgba(0,0,0,0.5);
|
|
}
|
|
|
|
.terminal-history-panel.open {
|
|
max-height: 350px;
|
|
opacity: 1;
|
|
}
|
|
|
|
.terminal-history-header {
|
|
padding: 0.75rem 1rem;
|
|
background: #1a1a2e;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
flex-shrink: 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.terminal-history-search {
|
|
flex: 1;
|
|
min-width: 180px;
|
|
display: flex;
|
|
align-items: center;
|
|
background: #0f0f1a;
|
|
border: 1px solid #374151;
|
|
border-radius: 0.5rem;
|
|
padding: 0 0.75rem;
|
|
gap: 0.5rem;
|
|
transition: border-color 0.15s, box-shadow 0.15s;
|
|
}
|
|
|
|
.terminal-history-search:focus-within {
|
|
border-color: #7c3aed;
|
|
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
|
}
|
|
|
|
.terminal-history-search i {
|
|
color: #6b7280;
|
|
}
|
|
|
|
.terminal-history-search input {
|
|
flex: 1;
|
|
background: transparent;
|
|
border: none;
|
|
outline: none;
|
|
color: #fff;
|
|
padding: 0.5rem 0;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.terminal-history-search input::placeholder {
|
|
color: #6b7280;
|
|
}
|
|
|
|
.terminal-history-clear-search {
|
|
background: none;
|
|
border: none;
|
|
color: #6b7280;
|
|
cursor: pointer;
|
|
padding: 0.25rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: color 0.15s;
|
|
}
|
|
|
|
.terminal-history-clear-search:hover {
|
|
color: #ef4444;
|
|
}
|
|
|
|
.terminal-history-filters {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.terminal-history-filter-select {
|
|
background: #0f0f1a;
|
|
border: 1px solid #374151;
|
|
border-radius: 0.375rem;
|
|
color: #e5e7eb;
|
|
padding: 0.375rem 0.5rem;
|
|
font-size: 0.75rem;
|
|
cursor: pointer;
|
|
outline: none;
|
|
transition: border-color 0.15s;
|
|
}
|
|
|
|
.terminal-history-filter-select:hover,
|
|
.terminal-history-filter-select:focus {
|
|
border-color: #7c3aed;
|
|
}
|
|
|
|
.terminal-history-filter-select option {
|
|
background: #1a1a2e;
|
|
}
|
|
|
|
.terminal-history-scope {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
font-size: 0.75rem;
|
|
color: #9ca3af;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.terminal-history-scope input {
|
|
width: 0.875rem;
|
|
height: 0.875rem;
|
|
accent-color: #7c3aed;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.terminal-history-filter-btn {
|
|
background: rgba(124,58,237,0.08);
|
|
border: 1px solid #374151;
|
|
border-radius: 0.375rem;
|
|
color: #9ca3af;
|
|
padding: 0.3rem 0.5rem;
|
|
font-size: 0.75rem;
|
|
cursor: pointer;
|
|
transition: background 0.15s, border-color 0.15s;
|
|
line-height: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.terminal-history-filter-btn:hover { background: rgba(124,58,237,0.18); border-color: #7c3aed; color: #e5e7eb; }
|
|
.terminal-history-filter-btn.active { background: rgba(124,58,237,0.3); border-color: #7c3aed; color: #fbbf24; }
|
|
|
|
/* Docked mode: panel stays visible and doesn't close on command execute */
|
|
.terminal-history-panel.docked {
|
|
position: relative;
|
|
top: 0;
|
|
max-height: 280px;
|
|
z-index: 1;
|
|
box-shadow: none;
|
|
border-bottom: 2px solid #7c3aed;
|
|
}
|
|
|
|
.terminal-history-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0.5rem;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: #374151 transparent;
|
|
}
|
|
|
|
.terminal-history-list::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.terminal-history-list::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.terminal-history-list::-webkit-scrollbar-thumb {
|
|
background: #374151;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.terminal-history-list::-webkit-scrollbar-thumb:hover {
|
|
background: #4b5563;
|
|
}
|
|
|
|
.terminal-history-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0.5rem 0.75rem;
|
|
border-radius: 0.375rem;
|
|
cursor: pointer;
|
|
transition: background 0.15s, transform 0.1s;
|
|
gap: 0.75rem;
|
|
border: 1px solid transparent;
|
|
}
|
|
|
|
.terminal-history-item:hover {
|
|
background: rgba(124, 58, 237, 0.1);
|
|
}
|
|
|
|
.terminal-history-item.selected {
|
|
background: rgba(124, 58, 237, 0.2);
|
|
border-color: rgba(124, 58, 237, 0.4);
|
|
}
|
|
|
|
.terminal-history-item:active {
|
|
transform: scale(0.99);
|
|
}
|
|
|
|
.terminal-history-cmd {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.terminal-history-cmd code {
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
font-size: 0.8125rem;
|
|
color: #a5b4fc;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
display: block;
|
|
}
|
|
|
|
.terminal-history-cmd code mark {
|
|
background: rgba(251, 191, 36, 0.3);
|
|
color: #fbbf24;
|
|
padding: 0 0.125rem;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.terminal-history-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.terminal-history-time {
|
|
font-size: 0.6875rem;
|
|
color: #6b7280;
|
|
cursor: help;
|
|
}
|
|
|
|
.terminal-history-host {
|
|
font-size: 0.6875rem;
|
|
color: #4ade80;
|
|
background: rgba(34, 197, 94, 0.1);
|
|
padding: 0.125rem 0.375rem;
|
|
border-radius: 4px;
|
|
border: 1px solid rgba(34, 197, 94, 0.2);
|
|
margin-left: auto;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.terminal-history-count {
|
|
font-size: 0.625rem;
|
|
color: #7c3aed;
|
|
background: rgba(124, 58, 237, 0.2);
|
|
padding: 0.125rem 0.375rem;
|
|
border-radius: 9999px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.terminal-history-host {
|
|
font-size: 0.6875rem;
|
|
color: #4ade80;
|
|
background: rgba(74, 222, 128, 0.1);
|
|
padding: 0.125rem 0.375rem;
|
|
border-radius: 0.25rem;
|
|
}
|
|
|
|
.terminal-history-actions-inline {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
opacity: 0;
|
|
transition: opacity 0.15s;
|
|
}
|
|
|
|
.terminal-history-item:hover .terminal-history-actions-inline,
|
|
.terminal-history-item.selected .terminal-history-actions-inline {
|
|
opacity: 1;
|
|
}
|
|
|
|
.terminal-history-action {
|
|
background: rgba(55, 65, 81, 0.5);
|
|
border: none;
|
|
color: #9ca3af;
|
|
padding: 0.375rem;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.75rem;
|
|
transition: background 0.15s, color 0.15s, transform 0.1s;
|
|
}
|
|
|
|
.terminal-history-action:hover {
|
|
background: rgba(124, 58, 237, 0.3);
|
|
color: #fff;
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.terminal-history-action-execute:hover {
|
|
background: rgba(34, 197, 94, 0.3);
|
|
color: #4ade80;
|
|
}
|
|
|
|
.terminal-history-empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 2rem;
|
|
color: #6b7280;
|
|
gap: 0.5rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.terminal-history-empty i {
|
|
font-size: 2rem;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.terminal-history-empty-hint {
|
|
font-size: 0.75rem;
|
|
color: #4b5563;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.terminal-history-loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 2rem;
|
|
color: #9ca3af;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.terminal-history-footer {
|
|
padding: 0.5rem 1rem;
|
|
background: #151520;
|
|
border-top: 1px solid #374151;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.terminal-history-hint {
|
|
font-size: 0.6875rem;
|
|
color: #6b7280;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.terminal-history-hint kbd {
|
|
background: #374151;
|
|
color: #e5e7eb;
|
|
padding: 0.125rem 0.375rem;
|
|
border-radius: 0.25rem;
|
|
font-family: inherit;
|
|
font-size: 0.625rem;
|
|
border: 1px solid #4b5563;
|
|
box-shadow: 0 1px 0 #4b5563;
|
|
}
|
|
|
|
#terminalHistoryBtn.active {
|
|
background: #7c3aed;
|
|
color: #fff;
|
|
}
|
|
|
|
/* Mobile responsive */
|
|
@media (max-width: 768px) {
|
|
.terminal-drawer-panel {
|
|
width: 100vw;
|
|
min-width: unset;
|
|
max-width: unset;
|
|
}
|
|
|
|
.terminal-host-ip {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* =====================================================
|
|
Session Limit Modal Styles
|
|
===================================================== */
|
|
.session-limit-modal {
|
|
max-width: 500px;
|
|
width: 90%;
|
|
}
|
|
|
|
.session-limit-message {
|
|
color: #e5e7eb;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.session-limit-reuse {
|
|
background: rgba(34, 197, 94, 0.1);
|
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
border-radius: 0.5rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.session-limit-reuse p {
|
|
color: #4ade80;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.session-limit-list {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.session-limit-list h4 {
|
|
color: #9ca3af;
|
|
font-size: 0.875rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.session-limit-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0.75rem;
|
|
background: rgba(55, 65, 81, 0.5);
|
|
border-radius: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.session-limit-item-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.session-limit-host {
|
|
color: #fff;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.session-limit-mode {
|
|
color: #9ca3af;
|
|
font-size: 0.75rem;
|
|
padding: 0.125rem 0.5rem;
|
|
background: rgba(107, 114, 128, 0.3);
|
|
border-radius: 0.25rem;
|
|
}
|
|
|
|
.session-limit-age {
|
|
color: #9ca3af;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.session-limit-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.5rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid #374151;
|
|
}
|
|
|
|
/* ==== Mission Control shell integration ==== */
|
|
#main-content { min-height: 100vh; }
|
|
|
|
#sidebar {
|
|
position: fixed;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: var(--sidebar-w);
|
|
background: linear-gradient(180deg, rgba(15, 22, 41, 0.95), rgba(10, 15, 30, 0.98));
|
|
backdrop-filter: var(--glass-blur);
|
|
border-right: var(--glass-border);
|
|
z-index: 40;
|
|
transition: width .35s cubic-bezier(.4,0,.2,1);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
#sidebar.collapsed { width: var(--sidebar-collapsed); }
|
|
#sidebar .nav-label,
|
|
#sidebar .logo-text { white-space: nowrap; overflow: hidden; opacity: 1; transition: opacity .25s; }
|
|
#sidebar.collapsed .nav-label,
|
|
#sidebar.collapsed .logo-text { opacity: 0; width: 0; }
|
|
#sidebar.collapsed .nav-item { justify-content: center; padding-left: 0; padding-right: 0; }
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 16px;
|
|
margin: 2px 8px;
|
|
border-radius: 10px;
|
|
color: var(--secondary-text);
|
|
cursor: pointer;
|
|
transition: all .2s;
|
|
font-size: .875rem;
|
|
font-weight: 500;
|
|
user-select: none;
|
|
}
|
|
.nav-item:hover { background: rgba(139,92,246,.08); color: var(--primary-text); }
|
|
.nav-item.active { background: rgba(139,92,246,.15); color: #a78bfa; box-shadow: inset 0 0 0 1px rgba(139,92,246,.25); }
|
|
.nav-item i { width: 20px; text-align: center; flex-shrink: 0; }
|
|
.nav-divider { height: 1px; background: linear-gradient(90deg, transparent, rgba(139,92,246,.2), transparent); margin: 8px 16px; }
|
|
|
|
[data-theme="light"] #sidebar,
|
|
body.light-theme #sidebar { background: linear-gradient(180deg, rgba(241,245,249,.97), rgba(226,232,240,.98)); }
|
|
[data-theme="light"] #header,
|
|
body.light-theme #header { background: rgba(241,245,249,.9); }
|
|
[data-theme="light"] .widget,
|
|
[data-theme="light"] .glass-card,
|
|
[data-theme="light"] .pro-card,
|
|
body.light-theme .widget,
|
|
body.light-theme .glass-card,
|
|
body.light-theme .pro-card { background: var(--bg-card); }
|
|
[data-theme="light"] .nav-item,
|
|
body.light-theme .nav-item { color: #475569; }
|
|
[data-theme="light"] .nav-item:hover,
|
|
body.light-theme .nav-item:hover { background: rgba(139,92,246,.06); color: #0f172a; }
|
|
[data-theme="light"] .nav-item.active,
|
|
body.light-theme .nav-item.active { background: rgba(139,92,246,.1); color: #7c3aed; }
|
|
|
|
/* ======== HEADER ======== */
|
|
#header {
|
|
position: fixed;
|
|
top: 0;
|
|
left: var(--sidebar-w);
|
|
right: 0;
|
|
height: var(--header-h);
|
|
background: rgba(10,15,30,.85);
|
|
backdrop-filter: var(--glass-blur);
|
|
border-bottom: var(--glass-border);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 0 24px;
|
|
z-index: 30;
|
|
transition: left .35s cubic-bezier(.4,0,.2,1);
|
|
}
|
|
.sys-metric {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: .75rem;
|
|
color: var(--secondary-text);
|
|
}
|
|
.sys-metric i { font-size: .8rem; }
|
|
.metric-val { font-weight: 600; color: var(--primary-text); }
|
|
|
|
.theme-toggle {
|
|
width: 42px;
|
|
height: 22px;
|
|
border-radius: 999px;
|
|
background: rgba(255,255,255,.08);
|
|
border: 1px solid rgba(255,255,255,.12);
|
|
cursor: pointer;
|
|
position: relative;
|
|
transition: background .3s;
|
|
flex-shrink: 0;
|
|
}
|
|
.theme-toggle .dot {
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 50%;
|
|
background: #818cf8;
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
transition: transform .3s, background .3s;
|
|
}
|
|
[data-theme="light"] .theme-toggle .dot,
|
|
body.light-theme .theme-toggle .dot { transform: translateX(20px); background: #f59e0b; }
|
|
|
|
/* ======== MAIN / PAGES ======== */
|
|
#main {
|
|
margin-left: var(--sidebar-w);
|
|
padding: calc(var(--header-h) + 24px) 28px 40px;
|
|
transition: margin-left .35s cubic-bezier(.4,0,.2,1);
|
|
}
|
|
.page-main-inner {
|
|
margin-left: var(--sidebar-w);
|
|
padding: calc(var(--header-h) + 24px) 28px 40px;
|
|
transition: margin-left .35s cubic-bezier(.4,0,.2,1);
|
|
}
|
|
.page-section { display: none; }
|
|
.page-section.active { display: block; }
|
|
|
|
/* ======== SIDEBAR COLLAPSED ======== */
|
|
body.sidebar-collapsed #sidebar { width: var(--sidebar-collapsed); }
|
|
body.sidebar-collapsed #sidebar .nav-label,
|
|
body.sidebar-collapsed #sidebar .logo-text { opacity: 0; width: 0; }
|
|
body.sidebar-collapsed #sidebar .nav-item { justify-content: center; padding-left: 0; padding-right: 0; }
|
|
body.sidebar-collapsed #header { left: var(--sidebar-collapsed); }
|
|
body.sidebar-collapsed #main,
|
|
body.sidebar-collapsed .page-main-inner,
|
|
body.sidebar-collapsed #page-help > div { margin-left: var(--sidebar-collapsed); }
|
|
|
|
/* ======== WIDGET GRID ======== */
|
|
.widget-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(var(--grid-cols, 3), 1fr);
|
|
gap: 20px;
|
|
}
|
|
.widget {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 16px;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
transition: border-color .25s, box-shadow .25s;
|
|
}
|
|
.widget:hover {
|
|
border-color: var(--border-hover);
|
|
box-shadow: 0 4px 24px rgba(0,0,0,.12);
|
|
}
|
|
.widget.span-2 { grid-column: span 2; }
|
|
.widget-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 14px 18px 10px;
|
|
}
|
|
.widget-body { padding: 0 18px 18px; }
|
|
.widget.dragging { opacity: .5; }
|
|
|
|
/* ======== KPI CARDS ======== */
|
|
.kpi-card {
|
|
background: rgba(255,255,255,.03);
|
|
border: 1px solid rgba(255,255,255,.06);
|
|
border-radius: 14px;
|
|
padding: 16px;
|
|
transition: border-color .2s, background .2s;
|
|
}
|
|
.kpi-card:hover { border-color: rgba(255,255,255,.12); background: rgba(255,255,255,.05); }
|
|
.kpi-value { font-family: 'Syne', sans-serif; font-size: 2rem; font-weight: 700; line-height: 1.1; margin-top: 6px; }
|
|
.kpi-label { font-size: .75rem; color: var(--secondary-text); margin-top: 2px; }
|
|
|
|
/* ======== FEED / ACTIVITY ======== */
|
|
.feed-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 0;
|
|
border-bottom: 1px solid rgba(255,255,255,.04);
|
|
}
|
|
.feed-item:last-child { border-bottom: none; }
|
|
.feed-time { font-size: .65rem; color: var(--secondary-text); min-width: 32px; text-align: right; }
|
|
.feed-badge {
|
|
font-size: .65rem;
|
|
padding: 2px 8px;
|
|
border-radius: 999px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: .03em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ======== STATUS DOT ======== */
|
|
.status-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
background: #6b7280;
|
|
}
|
|
.status-dot.online { background: #10b981; box-shadow: 0 0 6px rgba(16,185,129,.5); }
|
|
.status-dot.offline { background: #ef4444; }
|
|
|
|
/* ======== PROGRESS BAR ======== */
|
|
.progress-bar {
|
|
height: 6px;
|
|
border-radius: 999px;
|
|
background: rgba(255,255,255,.08);
|
|
overflow: hidden;
|
|
}
|
|
.progress-fill {
|
|
height: 100%;
|
|
border-radius: 999px;
|
|
transition: width .6s ease;
|
|
}
|
|
|
|
/* ======== SPARKLINE ======== */
|
|
.sparkline { display: block; flex-shrink: 0; }
|
|
|
|
/* ======== ACTION BUTTONS ======== */
|
|
.action-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(255,255,255,.03);
|
|
border: 1px solid rgba(255,255,255,.06);
|
|
border-radius: 14px;
|
|
padding: 12px;
|
|
cursor: pointer;
|
|
transition: all .2s;
|
|
color: var(--primary-text);
|
|
font-size: .8rem;
|
|
font-weight: 500;
|
|
}
|
|
.action-btn:hover { background: rgba(255,255,255,.07); border-color: rgba(255,255,255,.12); }
|
|
|
|
/* ======== COLUMN SELECTOR ======== */
|
|
.col-btn {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(255,255,255,.1);
|
|
background: transparent;
|
|
color: var(--secondary-text);
|
|
font-size: .75rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all .2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.col-btn:hover { border-color: rgba(139,92,246,.4); color: var(--primary-text); }
|
|
.col-btn.active { background: rgba(139,92,246,.2); border-color: rgba(139,92,246,.5); color: #a78bfa; }
|
|
|
|
/* ======== TOAST NOTIFICATIONS ======== */
|
|
.toast-container {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
z-index: 200;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
max-width: 380px;
|
|
}
|
|
.toast {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
padding: 14px 16px;
|
|
background: rgba(15,22,41,.92);
|
|
backdrop-filter: blur(12px);
|
|
border: 1px solid rgba(255,255,255,.08);
|
|
border-radius: 12px;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,.3);
|
|
overflow: hidden;
|
|
}
|
|
.toast-progress {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
height: 3px;
|
|
border-radius: 0 0 12px 12px;
|
|
}
|
|
|
|
/* ======== COMMAND PALETTE ======== */
|
|
#cmd-palette {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 200;
|
|
background: rgba(0,0,0,.6);
|
|
backdrop-filter: blur(8px);
|
|
display: none;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
padding-top: 15vh;
|
|
}
|
|
#cmd-palette.open { display: flex; }
|
|
#cmd-box {
|
|
width: 560px;
|
|
max-width: calc(100vw - 32px);
|
|
background: rgba(15,22,41,.95);
|
|
border: 1px solid rgba(255,255,255,.1);
|
|
border-radius: 16px;
|
|
box-shadow: 0 24px 64px rgba(0,0,0,.5);
|
|
overflow: hidden;
|
|
}
|
|
#cmd-input {
|
|
width: 100%;
|
|
padding: 16px 20px;
|
|
background: transparent;
|
|
border: none;
|
|
border-bottom: 1px solid rgba(255,255,255,.06);
|
|
color: var(--primary-text);
|
|
font-size: 1rem;
|
|
outline: none;
|
|
}
|
|
#cmd-input::placeholder { color: var(--secondary-text); }
|
|
#cmd-results { max-height: 320px; overflow-y: auto; padding: 8px; }
|
|
.cmd-result {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 10px 14px;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
transition: background .15s;
|
|
}
|
|
.cmd-result:hover { background: rgba(139,92,246,.12); }
|
|
|
|
/* ======== FOCUS OVERLAY ======== */
|
|
#focus-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 100;
|
|
background: rgba(0,0,0,.7);
|
|
backdrop-filter: blur(8px);
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 24px;
|
|
}
|
|
#focus-overlay.open { display: flex; }
|
|
#focus-content {
|
|
width: 100%;
|
|
max-width: 1000px;
|
|
max-height: 85vh;
|
|
overflow-y: auto;
|
|
background: rgba(15,22,41,.95);
|
|
border: 1px solid rgba(255,255,255,.1);
|
|
border-radius: 20px;
|
|
padding: 28px;
|
|
box-shadow: 0 24px 64px rgba(0,0,0,.5);
|
|
}
|
|
|
|
/* ======== CUSTOMIZE DRAWER ======== */
|
|
#drawer-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0,0,0,.5);
|
|
z-index: 90;
|
|
display: none;
|
|
}
|
|
#drawer-overlay.open { display: block; }
|
|
#customize-drawer {
|
|
position: fixed;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 340px;
|
|
background: rgba(15,22,41,.97);
|
|
border-left: 1px solid rgba(255,255,255,.08);
|
|
z-index: 95;
|
|
padding: 24px;
|
|
overflow-y: auto;
|
|
}
|
|
#customize-drawer.open { transform: translateX(0); }
|
|
|
|
.pro-btn-secondary {
|
|
padding: 8px 16px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(255,255,255,.1);
|
|
background: transparent;
|
|
color: var(--secondary-text);
|
|
font-size: .8rem;
|
|
cursor: pointer;
|
|
transition: all .2s;
|
|
}
|
|
.pro-btn-secondary:hover { border-color: rgba(139,92,246,.4); color: var(--primary-text); }
|
|
|
|
/* ======== HOST ACTION BUTTONS ======== */
|
|
.host-action-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 18px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
color: white;
|
|
}
|
|
.host-action-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
|
|
.host-action-btn:active { transform: translateY(0); }
|
|
|
|
.host-action-btn-metrics { background: linear-gradient(135deg, #3b82f6 0%, #06b6d4 100%); }
|
|
.host-action-btn-metrics:hover { box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); }
|
|
|
|
.host-action-btn-playbook { background: linear-gradient(135deg, #10b981 0%, #34d399 100%); }
|
|
.host-action-btn-playbook:hover { box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4); }
|
|
|
|
.host-action-btn-import { background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%); }
|
|
.host-action-btn-import:hover { box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4); }
|
|
|
|
.host-action-btn-refresh { background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%); }
|
|
.host-action-btn-refresh:hover { box-shadow: 0 4px 12px rgba(245, 158, 11, 0.4); }
|
|
|
|
/* Per-host action buttons */
|
|
.host-card-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
margin-top: 12px;
|
|
padding-top: 12px;
|
|
border-top: 1px solid rgba(255,255,255,0.05);
|
|
}
|
|
|
|
.host-card-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 6px 10px;
|
|
border-radius: 6px;
|
|
border: none;
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.3px;
|
|
}
|
|
|
|
.host-card-btn:hover:not(:disabled) { transform: translateY(-1px); }
|
|
.host-card-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
|
.host-card-btn-health { background: rgba(139, 92, 246, 0.15); color: #a78bfa; border: 1px solid rgba(139, 92, 246, 0.3); }
|
|
.host-card-btn-health:hover:not(:disabled) { background: rgba(139, 92, 246, 0.25); box-shadow: 0 2px 8px rgba(139, 92, 246, 0.2); }
|
|
|
|
.host-card-btn-upgrade { background: rgba(16, 185, 129, 0.15); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.3); }
|
|
.host-card-btn-upgrade:hover:not(:disabled) { background: rgba(16, 185, 129, 0.25); box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2); }
|
|
|
|
.host-card-btn-reboot { background: rgba(245, 158, 11, 0.15); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.3); }
|
|
.host-card-btn-reboot:hover:not(:disabled) { background: rgba(245, 158, 11, 0.25); box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2); }
|
|
|
|
.host-card-btn-backup { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); }
|
|
.host-card-btn-backup:hover:not(:disabled) { background: rgba(59, 130, 246, 0.25); box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2); }
|
|
|
|
.host-card-btn-bootstrap { background: rgba(234, 179, 8, 0.15); color: #facc15; border: 1px solid rgba(234, 179, 8, 0.3); }
|
|
.host-card-btn-bootstrap:hover:not(:disabled) { background: rgba(234, 179, 8, 0.25); box-shadow: 0 2px 8px rgba(234, 179, 8, 0.2); }
|
|
.host-card-btn-bootstrap.configured { background: rgba(107, 114, 128, 0.15); color: #9ca3af; border: 1px solid rgba(107, 114, 128, 0.3); }
|
|
|
|
.host-card-btn-terminal { background: rgba(6, 182, 212, 0.15); color: #22d3ee; border: 1px solid rgba(6, 182, 212, 0.3); }
|
|
.host-card-btn-terminal:hover:not(:disabled) { background: rgba(6, 182, 212, 0.25); box-shadow: 0 2px 8px rgba(6, 182, 212, 0.2); }
|
|
|
|
.host-card-btn-popout { background: rgba(107, 114, 128, 0.15); color: #9ca3af; border: 1px solid rgba(107, 114, 128, 0.3); }
|
|
.host-card-btn-popout:hover:not(:disabled) { background: rgba(107, 114, 128, 0.25); }
|
|
|
|
/* Light theme overrides for host action buttons */
|
|
[data-theme="light"] .host-action-btn-metrics,
|
|
body.light-theme .host-action-btn-metrics { background: linear-gradient(135deg, #2563eb 0%, #0891b2 100%); }
|
|
[data-theme="light"] .host-action-btn-playbook,
|
|
body.light-theme .host-action-btn-playbook { background: linear-gradient(135deg, #059669 0%, #10b981 100%); }
|
|
[data-theme="light"] .host-action-btn-import,
|
|
body.light-theme .host-action-btn-import { background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%); }
|
|
[data-theme="light"] .host-action-btn-refresh,
|
|
body.light-theme .host-action-btn-refresh { background: linear-gradient(135deg, #d97706 0%, #f59e0b 100%); }
|
|
|
|
[data-theme="light"] .host-card-btn-health,
|
|
body.light-theme .host-card-btn-health { background: rgba(139, 92, 246, 0.1); color: #7c3aed; border-color: rgba(139, 92, 246, 0.2); }
|
|
[data-theme="light"] .host-card-btn-upgrade,
|
|
body.light-theme .host-card-btn-upgrade { background: rgba(16, 185, 129, 0.1); color: #059669; border-color: rgba(16, 185, 129, 0.2); }
|
|
[data-theme="light"] .host-card-btn-reboot,
|
|
body.light-theme .host-card-btn-reboot { background: rgba(245, 158, 11, 0.1); color: #d97706; border-color: rgba(245, 158, 11, 0.2); }
|
|
[data-theme="light"] .host-card-btn-backup,
|
|
body.light-theme .host-card-btn-backup { background: rgba(59, 130, 246, 0.1); color: #2563eb; border-color: rgba(59, 130, 246, 0.2); }
|
|
[data-theme="light"] .host-card-btn-bootstrap,
|
|
body.light-theme .host-card-btn-bootstrap { background: rgba(234, 179, 8, 0.1); color: #b45309; border-color: rgba(234, 179, 8, 0.2); }
|
|
[data-theme="light"] .host-card-btn-terminal,
|
|
body.light-theme .host-card-btn-terminal { background: rgba(6, 182, 212, 0.1); color: #0891b2; border-color: rgba(6, 182, 212, 0.2); }
|
|
[data-theme="light"] .host-card-btn-popout,
|
|
body.light-theme .host-card-btn-popout { background: rgba(107, 114, 128, 0.1); color: #4b5563; border-color: rgba(107, 114, 128, 0.2); }
|
|
|
|
/* ======== LIGHT THEME WIDGET OVERRIDES ======== */
|
|
body.light-theme #cmd-box { background: rgba(255,255,255,.97); border-color: rgba(0,0,0,.1); }
|
|
[data-theme="light"] #cmd-input,
|
|
body.light-theme #cmd-input { border-color: rgba(0,0,0,.08); }
|
|
[data-theme="light"] #focus-content,
|
|
body.light-theme #focus-content { background: rgba(255,255,255,.97); border-color: rgba(0,0,0,.1); }
|
|
[data-theme="light"] #customize-drawer,
|
|
body.light-theme #customize-drawer { background: rgba(255,255,255,.97); border-color: rgba(0,0,0,.08); }
|
|
[data-theme="light"] .action-btn,
|
|
body.light-theme .action-btn { background: rgba(0,0,0,.03); border-color: rgba(0,0,0,.08); }
|
|
|
|
@media (max-width: 1024px) {
|
|
#sidebar { width: var(--sidebar-collapsed); }
|
|
#sidebar .nav-label,
|
|
#sidebar .logo-text { opacity: 0; width: 0; }
|
|
#header { left: var(--sidebar-collapsed); }
|
|
#main,
|
|
.page-main-inner,
|
|
#page-help > div { margin-left: var(--sidebar-collapsed); }
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
#header { padding: 0 14px; gap: 10px; }
|
|
.sys-metric span:not(.metric-val) { display: none; }
|
|
#cmd-box { width: calc(100vw - 24px); }
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.widget-grid { --grid-cols: 1 !important; }
|
|
.widget.span-2 { grid-column: span 1; }
|
|
#focus-overlay { padding: 14px; }
|
|
#focus-content { padding: 20px; }
|
|
}
|
|
</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 flex items-center justify-center mx-auto mb-4">
|
|
<img src="/static/icons/logo-transparent.png" alt="Homelab Logo" class="w-full h-full object-contain drop-shadow-lg">
|
|
</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 flex items-center justify-center mx-auto mb-4">
|
|
<img src="/static/icons/logo-transparent.png" alt="Homelab Logo" class="w-full h-full object-contain drop-shadow-lg">
|
|
</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="8"
|
|
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="Min. 8 caractères, Maj, Min, Chiffre, Spécial"
|
|
>
|
|
<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="8"
|
|
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">
|
|
|
|
<!-- Mobile Navigation -->
|
|
<div id="mobile-nav-overlay" class="mobile-nav-overlay" onclick="closeMobileNav()"></div>
|
|
<aside id="mobile-nav-sidebar" class="mobile-nav-sidebar" aria-label="Navigation mobile">
|
|
<div class="mobile-nav-header">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500 to-cyan-400 flex items-center justify-center">
|
|
<i class="fas fa-satellite-dish text-white text-xs"></i>
|
|
</div>
|
|
<span class="font-heading font-semibold text-white">Mission Control</span>
|
|
</div>
|
|
<button type="button" class="w-9 h-9 rounded-lg bg-white/5 hover:bg-white/10 text-gray-300" onclick="closeMobileNav()" aria-label="Fermer le menu">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="mobile-nav-links">
|
|
<button type="button" class="mobile-nav-link active" data-page="dashboard" onclick="mobileNavigateTo('dashboard')"><i class="fas fa-grip"></i><span>Dashboard</span></button>
|
|
<button type="button" class="mobile-nav-link" data-page="hosts" onclick="mobileNavigateTo('hosts')"><i class="fas fa-server"></i><span>Hosts</span></button>
|
|
<button type="button" class="mobile-nav-link" data-page="playbooks" onclick="mobileNavigateTo('playbooks')"><i class="fas fa-book"></i><span>Playbooks</span></button>
|
|
<button type="button" class="mobile-nav-link" data-page="tasks" onclick="mobileNavigateTo('tasks')"><i class="fas fa-list-check"></i><span>Taches</span></button>
|
|
<button type="button" class="mobile-nav-link" data-page="schedules" onclick="mobileNavigateTo('schedules')"><i class="fas fa-calendar-alt"></i><span>Planificateur</span></button>
|
|
<button type="button" class="mobile-nav-link" data-page="docker" onclick="mobileNavigateTo('docker')"><i class="fab fa-docker"></i><span>Docker</span></button>
|
|
<button type="button" class="mobile-nav-link" data-page="logs" onclick="mobileNavigateTo('logs')"><i class="fas fa-file-alt"></i><span>Logs</span></button>
|
|
<button type="button" class="mobile-nav-link" data-page="alerts" onclick="mobileNavigateTo('alerts')"><i class="fas fa-bell"></i><span>Alertes</span></button>
|
|
<button type="button" class="mobile-nav-link" data-page="configuration" onclick="mobileNavigateTo('configuration')"><i class="fas fa-cog"></i><span>Parametres</span></button>
|
|
<button type="button" class="mobile-nav-link" data-page="help" onclick="mobileNavigateTo('help')"><i class="fas fa-circle-info"></i><span>Aide</span></button>
|
|
<button type="button" id="theme-toggle-mobile" class="mobile-nav-link" onclick="toggleTheme()"><i id="mobile-theme-icon" class="fas fa-moon text-gray-300"></i><span id="mobile-theme-label">Theme sombre</span></button>
|
|
<button type="button" class="mobile-nav-link text-red-300 hover:text-red-200" onclick="handleLogout()"><i class="fas fa-door-open"></i><span>Deconnexion</span></button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- SIDEBAR -->
|
|
<div id="sidebar">
|
|
<div class="flex items-center gap-3 px-5 py-4 border-b border-white/5">
|
|
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-cyan-400 flex items-center justify-center flex-shrink-0">
|
|
<i class="fas fa-satellite-dish text-white text-sm"></i>
|
|
</div>
|
|
<span class="logo-text font-heading font-bold text-base text-white tracking-tight">Mission Control</span>
|
|
</div>
|
|
<nav class="flex-1 py-3 overflow-y-auto" id="sidebar-nav">
|
|
<div class="px-4 mb-2"><span class="nav-label text-[.65rem] font-semibold uppercase tracking-wider text-white/25">Navigation</span></div>
|
|
<div class="nav-item active" data-page="dashboard" onclick="navigateTo('dashboard')"><i class="fas fa-grip"></i><span class="nav-label">Dashboard</span></div>
|
|
<div class="nav-item" data-page="hosts" onclick="navigateTo('hosts')"><i class="fas fa-server"></i><span class="nav-label">Hosts</span></div>
|
|
<div class="nav-item" data-page="playbooks" onclick="navigateTo('playbooks')"><i class="fas fa-book"></i><span class="nav-label">Playbooks</span></div>
|
|
<div class="nav-item" data-page="tasks" onclick="navigateTo('tasks')"><i class="fas fa-list-check"></i><span class="nav-label">Tâches</span></div>
|
|
<div class="nav-item" data-page="schedules" onclick="navigateTo('schedules')"><i class="fas fa-calendar-alt"></i><span class="nav-label">Planificateur</span></div>
|
|
<div class="nav-item" data-page="docker" onclick="navigateTo('docker')"><i class="fab fa-docker"></i><span class="nav-label">Docker</span></div>
|
|
<div class="nav-divider"></div>
|
|
<div class="px-4 mb-2 mt-2"><span class="nav-label text-[.65rem] font-semibold uppercase tracking-wider text-white/25">Système</span></div>
|
|
<div class="nav-item" data-page="logs" onclick="navigateTo('logs')"><i class="fas fa-file-alt"></i><span class="nav-label">Logs</span></div>
|
|
<div class="nav-item" data-page="alerts" onclick="navigateTo('alerts')"><i class="fas fa-bell"></i><span class="nav-label">Alertes</span></div>
|
|
<div class="nav-item" data-page="configuration" onclick="navigateTo('configuration')"><i class="fas fa-cog"></i><span class="nav-label">Paramètres</span></div>
|
|
</nav>
|
|
<div class="p-3 border-t border-white/5">
|
|
<div class="nav-item" onclick="toggleSidebar()" id="sidebar-toggle"><i class="fas fa-angles-left"></i><span class="nav-label">Réduire</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- HEADER -->
|
|
<header id="header">
|
|
<button id="mobile-menu-btn" class="mobile-menu-btn" onclick="toggleMobileNav()" aria-label="Ouvrir le menu">
|
|
<span class="hamburger-icon"><span></span><span></span><span></span></span>
|
|
</button>
|
|
<div class="flex items-center gap-4 flex-1">
|
|
<div class="sys-metric"><i class="fas fa-microchip text-cyan-400"></i><span>CPU</span><span class="metric-val" id="sys-cpu">—</span></div>
|
|
<div class="sys-metric"><i class="fas fa-memory text-violet-400"></i><span>RAM</span><span class="metric-val" id="sys-ram">—</span></div>
|
|
<div class="sys-metric"><i class="fas fa-network-wired text-green-400"></i><span>Réseau</span><span class="metric-val" id="sys-net">—</span></div>
|
|
<div class="sys-metric" id="ws-status"><span class="status-dot" id="ws-dot"></span><span id="ws-label">WS</span></div>
|
|
</div>
|
|
<button onclick="openCmdPalette()" class="flex items-center gap-2 px-3 py-1.5 rounded-lg border border-white/10 text-sm text-gray-400 hover:border-violet-500/40 hover:text-gray-200 transition-all" title="Ctrl+K">
|
|
<i class="fas fa-search text-xs"></i><span class="hidden sm:inline">Rechercher...</span><kbd class="ml-2 text-[.65rem] bg-white/5 px-1.5 py-0.5 rounded border border-white/10">⌘K</kbd>
|
|
</button>
|
|
<div class="theme-toggle" onclick="toggleTheme()" title="Changer de thème"><div class="dot"></div></div>
|
|
<button onclick="handleLogout()" class="text-gray-400 hover:text-red-400 transition-colors ml-2" title="Déconnexion"><i class="fas fa-door-open"></i></button>
|
|
</header>
|
|
|
|
<!-- ==================== PAGE: DASHBOARD ==================== -->
|
|
<section id="page-dashboard" class="page-section active">
|
|
<main id="main">
|
|
<!-- Toolbar -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 class="font-heading text-2xl font-bold tracking-tight">Dashboard</h1>
|
|
<p class="text-sm text-gray-500 mt-1">Vue d'ensemble de votre infrastructure</p>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-xs text-gray-500 mr-2">Colonnes</span>
|
|
<button class="col-btn" data-cols="2" onclick="setGridCols(2)">2</button>
|
|
<button class="col-btn active" data-cols="3" onclick="setGridCols(3)">3</button>
|
|
<button class="col-btn" data-cols="4" onclick="setGridCols(4)">4</button>
|
|
</div>
|
|
</div>
|
|
<!-- Widget Grid -->
|
|
<div class="widget-grid" id="widget-grid"></div>
|
|
</main>
|
|
</section>
|
|
<!-- END PAGE: DASHBOARD -->
|
|
|
|
<!-- COMMAND PALETTE -->
|
|
<div id="cmd-palette" onclick="if(event.target===this)closeCmdPalette()">
|
|
<div id="cmd-box">
|
|
<input id="cmd-input" type="text" placeholder="Rechercher hosts, containers, actions..." autocomplete="off">
|
|
<div id="cmd-results"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FOCUS OVERLAY -->
|
|
<div id="focus-overlay" onclick="if(event.target===this)closeFocus()">
|
|
<div id="focus-content"></div>
|
|
</div>
|
|
|
|
<!-- CUSTOMIZE DRAWER -->
|
|
<div id="drawer-overlay" onclick="closeCustomizeDrawer()"></div>
|
|
<div id="customize-drawer">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h3 class="font-heading text-lg font-bold">Personnaliser</h3>
|
|
<button onclick="closeCustomizeDrawer()" class="text-gray-400 hover:text-white"><i class="fas fa-xmark text-lg"></i></button>
|
|
</div>
|
|
<p class="text-sm text-gray-500 mb-4">Activez ou désactivez les widgets affichés sur votre dashboard.</p>
|
|
<div id="widget-toggles"></div>
|
|
<div class="mt-6 pt-4 border-t border-white/5">
|
|
<button onclick="resetLayout()" class="action-btn w-full justify-center text-sm"><i class="fas fa-rotate-left"></i> Réinitialiser la disposition</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TOAST CONTAINER -->
|
|
<div class="toast-container" id="toast-container"></div>
|
|
|
|
<!-- ==================== PAGE: HOSTS ==================== -->
|
|
<section id="page-hosts" class="page-section">
|
|
<main class="page-main-inner">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<div>
|
|
<h1 class="font-heading text-2xl font-bold tracking-tight">Inventaire des Hosts</h1>
|
|
<p class="text-sm text-gray-500 mt-1 flex items-center gap-2">
|
|
<span id="hosts-stat-total" class="flex items-center gap-1.5"><i class="fas fa-server text-xs"></i> <span>—</span> hôtes</span>
|
|
<span class="w-1 h-1 rounded-full bg-gray-800"></span>
|
|
<span class="text-green-500/80" id="hosts-stat-online">0 actifs</span>
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button type="button" class="pro-btn pro-btn-secondary" onclick="dashboard.syncHostsFromAnsible()">
|
|
<i class="fas fa-download"></i> Importer Ansible
|
|
</button>
|
|
<button type="button" class="pro-btn pro-btn-primary" onclick="dashboard.addHost()">
|
|
<i class="fas fa-plus"></i> Ajouter Host
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Global Action Buttons -->
|
|
<div class="flex flex-wrap gap-3 mb-6">
|
|
<button type="button" class="host-action-btn host-action-btn-metrics" onclick="dashboard.collectAllMetrics()">
|
|
<i class="fas fa-chart-line"></i> Collecter Métriques
|
|
</button>
|
|
<button type="button" class="host-action-btn host-action-btn-playbook" onclick="dashboard.showGlobalPlaybookModal()">
|
|
<i class="fas fa-play"></i> Playbook
|
|
</button>
|
|
<button type="button" class="host-action-btn host-action-btn-import" onclick="dashboard.syncHostsFromAnsible()">
|
|
<i class="fas fa-download"></i> Importer Ansible
|
|
</button>
|
|
<button type="button" class="host-action-btn host-action-btn-refresh" onclick="dashboard.loadAllData()">
|
|
<i class="fas fa-sync-alt"></i> Rafraîchir
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-4 mb-8 rounded-xl bg-white/[.03] border border-white/[.06] backdrop-blur-sm">
|
|
<div class="flex flex-col sm:flex-row items-center gap-4">
|
|
<!-- Search -->
|
|
<div class="relative flex-1 group">
|
|
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 group-focus-within:text-violet-500 transition-colors"></i>
|
|
<input type="text"
|
|
data-hosts-search="true"
|
|
placeholder="Rechercher par nom, IP, tag..."
|
|
class="w-full bg-[#1a1d27] border border-white/5 rounded-lg py-2.5 pl-10 pr-4 text-sm focus:outline-none focus:border-violet-500/50 transition-all font-mono"
|
|
oninput="dashboard.currentHostsSearch = this.value; dashboard.renderHosts()">
|
|
</div>
|
|
|
|
<!-- Quick Filters -->
|
|
<div class="flex items-center gap-2">
|
|
<div class="flex items-center gap-1 bg-[#1a1d27] p-1 rounded-lg border border-white/5">
|
|
<button onclick="dashboard.filterHostsByBootstrap('all')" id="btn-host-filter-all" class="px-3 py-1.5 text-[10px] font-bold tracking-wider uppercase rounded hover:bg-white/5 transition-all text-gray-400">Tous</button>
|
|
<button onclick="dashboard.filterHostsByBootstrap('ready')" id="btn-host-filter-ready" class="px-3 py-1.5 text-[10px] font-bold tracking-wider uppercase rounded hover:bg-white/5 transition-all text-gray-400">Ready</button>
|
|
<button onclick="dashboard.filterHostsByBootstrap('not_configured')" id="btn-host-filter-not" class="px-3 py-1.5 text-[10px] font-bold tracking-wider uppercase rounded hover:bg-white/5 transition-all text-gray-400">Pending</button>
|
|
</div>
|
|
<select id="host-group-filter-select" onchange="dashboard.currentGroupFilter = this.value; dashboard.renderHosts()" class="bg-[#1a1d27] border border-white/5 rounded-lg py-2.5 px-3 text-xs focus:outline-none focus:border-violet-500/50 transition-all text-gray-300">
|
|
<option value="all">Tous les groupes</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="hosts-page-list" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
<!-- Hosts cards go here -->
|
|
</div>
|
|
</main>
|
|
</section>
|
|
<!-- END PAGE: HOSTS -->
|
|
|
|
<!-- ==================== PAGE: PLAYBOOKS ==================== -->
|
|
<section id="page-playbooks" class="page-section">
|
|
<main class="page-main-inner">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<div>
|
|
<h1 class="font-heading text-2xl font-bold tracking-tight">Playbooks</h1>
|
|
<p class="text-sm text-gray-500 mt-1 flex items-center gap-2">
|
|
<span id="playbooks-stat-count" class="flex items-center gap-1.5"><i class="fas fa-book-open text-xs"></i> <span>—</span> modules</span>
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button type="button" class="pro-btn pro-btn-secondary" onclick="dashboard.refreshPlaybooks()">
|
|
<i class="fas fa-sync-alt"></i> Rafraîchir
|
|
</button>
|
|
<button type="button" class="pro-btn pro-btn-primary" onclick="dashboard.showCreatePlaybookModal()">
|
|
<i class="fas fa-plus"></i> Nouveau Playbook
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-4 mb-8 rounded-xl bg-white/[.03] border border-white/[.06] backdrop-blur-sm">
|
|
<div class="flex flex-col sm:flex-row items-center gap-4">
|
|
<!-- Search -->
|
|
<div class="relative flex-1 group">
|
|
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 group-focus-within:text-violet-500 transition-colors"></i>
|
|
<input type="text" id="playbook-search-input"
|
|
placeholder="Rechercher par nom ou description..."
|
|
class="w-full bg-[#1a1d27] border border-white/5 rounded-lg py-2.5 pl-10 pr-4 text-sm focus:outline-none focus:border-violet-500/50 transition-all font-mono"
|
|
oninput="dashboard.filterPlaybooks(this.value)">
|
|
</div>
|
|
|
|
<!-- Quick Filters -->
|
|
<div class="flex items-center gap-2">
|
|
<div id="playbook-category-filters" class="flex items-center gap-1 bg-[#1a1d27] p-1 rounded-lg border border-white/5 overflow-x-auto scrollbar-hide max-w-[50vw]">
|
|
<button onclick="dashboard.filterPlaybooksByCategory('all')" class="px-3 py-1.5 text-[10px] font-bold tracking-wider uppercase rounded hover:bg-white/5 transition-all text-violet-400" data-category="all">Toutes</button>
|
|
<button onclick="dashboard.filterPlaybooksByCategory('maintenance')" class="px-3 py-1.5 text-[10px] font-bold tracking-wider uppercase rounded hover:bg-white/5 transition-all text-gray-400" data-category="maintenance">Maintenance</button>
|
|
<button onclick="dashboard.filterPlaybooksByCategory('deploy')" class="px-3 py-1.5 text-[10px] font-bold tracking-wider uppercase rounded hover:bg-white/5 transition-all text-gray-400" data-category="deploy">Déploiement</button>
|
|
<button onclick="dashboard.filterPlaybooksByCategory('backup')" class="px-3 py-1.5 text-[10px] font-bold tracking-wider uppercase rounded hover:bg-white/5 transition-all text-gray-400" data-category="backup">Sauvegardes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="playbooks-list" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
<!-- Playbooks populate here -->
|
|
</div>
|
|
</main>
|
|
</section>
|
|
<!-- END PAGE: PLAYBOOKS -->
|
|
|
|
<!-- ==================== PAGE: TASKS ==================== -->
|
|
<section id="page-tasks" class="page-section">
|
|
<main class="page-main-inner">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<div>
|
|
<h1 class="font-heading text-2xl font-bold tracking-tight">File d'exécution</h1>
|
|
<p class="text-sm text-gray-500 mt-1 flex items-center gap-2">
|
|
<span id="tasks-stat-count" class="flex items-center gap-1.5"><i class="fas fa-list-check text-xs"></i> <span>—</span> tâches</span>
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button type="button" class="pro-btn pro-btn-secondary" onclick="dashboard.refreshTaskLogs()">
|
|
<i class="fas fa-sync-alt"></i> Rafraîchir
|
|
</button>
|
|
<button type="button" class="pro-btn pro-btn-secondary" onclick="dashboard.clearDateFilters()">
|
|
<i class="fas fa-times"></i> Reset Filtres
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
<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="hidden mt-6 text-center">
|
|
<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>
|
|
</main>
|
|
</section>
|
|
<!-- END PAGE: TASKS -->
|
|
|
|
<!-- ==================== PAGE: SCHEDULES ==================== -->
|
|
<section id="page-schedules" class="page-section">
|
|
<main class="page-main-inner">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<div>
|
|
<h1 class="font-heading text-2xl font-bold tracking-tight">Planificateur</h1>
|
|
<p class="text-sm text-gray-500 mt-1 flex items-center gap-2">
|
|
<span class="flex items-center gap-1.5"><i class="fas fa-clock text-xs"></i> Automation orchestrée</span>
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button type="button" class="pro-btn pro-btn-secondary" onclick="dashboard.refreshSchedules()">
|
|
<i class="fas fa-sync-alt"></i>
|
|
</button>
|
|
<button type="button" class="pro-btn pro-btn-primary" onclick="showCreateScheduleModal()">
|
|
<i class="fas fa-plus"></i> Nouveau Schedule
|
|
</button>
|
|
</div>
|
|
</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>
|
|
</main>
|
|
</section>
|
|
<!-- END PAGE: SCHEDULES -->
|
|
|
|
<section id="page-alerts" class="page-section">
|
|
<main class="page-main-inner">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<div>
|
|
<h1 class="font-heading text-2xl font-bold tracking-tight">Centre d'Alertes</h1>
|
|
<p class="text-sm text-gray-500 mt-1 flex items-center gap-2">
|
|
<span id="alerts-stat-unread" class="flex items-center gap-1.5"><i class="fas fa-bell text-xs"></i> <span>—</span> nouvelles notifications</span>
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button type="button" class="pro-btn pro-btn-secondary" onclick="dashboard.markAllAlertsRead()">
|
|
<i class="fas fa-check-double"></i> Tout lire
|
|
</button>
|
|
<button type="button" class="pro-btn pro-btn-secondary text-red-400 hover:text-red-300" onclick="dashboard.clearAllAlerts()">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</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>
|
|
</main>
|
|
</section>
|
|
|
|
<!-- ==================== PAGE: DOCKER ==================== -->
|
|
<section id="page-docker" class="page-section">
|
|
<main class="page-main-inner">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<div>
|
|
<h1 class="font-heading text-2xl font-bold tracking-tight">Docker Infrastructure</h1>
|
|
<p class="text-sm text-gray-500 mt-1 flex items-center gap-2">
|
|
<span id="docker-stat-summary" class="flex items-center gap-1.5"><i class="fab fa-docker text-xs text-blue-400"></i> Ordonnancement de containers</span>
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button type="button" class="pro-btn pro-btn-secondary" onclick="dockerSection.collectAll()">
|
|
<i class="fas fa-sync"></i> Collecter Tout
|
|
</button>
|
|
<button type="button" class="pro-btn pro-btn-secondary" onclick="dockerSection.showAlerts()">
|
|
<i class="fas fa-bell"></i> Alertes <span id="docker-alerts-badge-btn" class="ml-1 opacity-50">0</span>
|
|
</button>
|
|
</div>
|
|
</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>
|
|
</main>
|
|
</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-xs font-mono text-gray-300 whitespace-pre-wrap overflow-x-auto max-h-[60vh]">Chargement...</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== PAGE: DOCKER CONTAINERS (ALL HOSTS) ==================== -->
|
|
<section id="page-docker-containers" class="page-section">
|
|
<main class="page-main-inner">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<div class="flex items-center gap-4">
|
|
<button onclick="navigateTo('docker')" class="w-10 h-10 flex items-center justify-center rounded-xl bg-white/5 border border-white/5 hover:bg-white/10 transition-all">
|
|
<i class="fas fa-chevron-left text-xs text-gray-400"></i>
|
|
</button>
|
|
<div>
|
|
<h1 class="font-heading text-2xl font-bold tracking-tight">Gestion des Containers</h1>
|
|
<p class="text-sm text-gray-500 mt-1 flex items-center gap-2">
|
|
<span id="containers-stat-active" class="flex items-center gap-1.5"><i class="fas fa-box text-xs"></i> <span>—</span> instances actives</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<span id="containers-last-update" class="text-[10px] font-mono text-gray-600 uppercase tracking-widest leading-none">Actualisé: —</span>
|
|
<button type="button" class="pro-btn pro-btn-secondary" onclick="containersPage.refresh()">
|
|
<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-filter-favorites" class="p-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors" title="Favoris uniquement" aria-pressed="false">
|
|
<i class="far fa-star"></i>
|
|
</button>
|
|
<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>
|
|
</main>
|
|
</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-icon"></span>
|
|
<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">
|
|
<main class="page-main-inner">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<div>
|
|
<h1 class="font-heading text-2xl font-bold tracking-tight">Configuration</h1>
|
|
<p class="text-sm text-gray-500 mt-1 flex items-center gap-2">
|
|
<span class="flex items-center gap-1.5"><i class="fas fa-cog text-xs"></i> Paramètres système et administration</span>
|
|
</p>
|
|
</div>
|
|
</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>
|
|
</main>
|
|
</section>
|
|
|
|
<!-- ==================== PAGE: LOGS ==================== -->
|
|
<section id="page-logs" class="page-section">
|
|
<main class="page-main-inner">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<div>
|
|
<h1 class="font-heading text-2xl font-bold tracking-tight">Logs Système</h1>
|
|
<p class="text-sm text-gray-500 mt-1 flex items-center gap-2">
|
|
<span class="flex items-center gap-1.5"><i class="fas fa-file-alt text-xs"></i> Historique des événements</span>
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button type="button" class="pro-btn pro-btn-secondary" onclick="dashboard.refreshSystemLogs()">
|
|
<i class="fas fa-sync"></i>
|
|
</button>
|
|
<button type="button" class="pro-btn pro-btn-secondary" onclick="dashboard.exportLogs()">
|
|
<i class="fas fa-download"></i> Exporter
|
|
</button>
|
|
</div>
|
|
</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>
|
|
</main>
|
|
</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 src="/static/dashboard_pro.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);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Initialize navigation
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Setup nav link clicks
|
|
document.querySelectorAll('.nav-item[data-page]').forEach(link => {
|
|
link.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
navigateTo(this.dataset.page);
|
|
});
|
|
});
|
|
|
|
// Logo click returns to dashboard
|
|
const logo = document.querySelector('#sidebar > .flex.items-center.gap-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-item[data-page="dashboard"]')?.classList.add('active');
|
|
document.querySelector('.mobile-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 (!overlay || !sidebar) return;
|
|
|
|
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');
|
|
if (!overlay || !sidebar) return;
|
|
|
|
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 currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
document.documentElement.setAttribute('data-theme', newTheme);
|
|
document.body.classList.toggle('light-theme', newTheme === 'light');
|
|
localStorage.setItem('theme', newTheme);
|
|
|
|
// Update all theme toggle icons
|
|
const themeIcons = document.querySelectorAll('#theme-toggle i, #theme-toggle-mobile i, #mobile-theme-icon');
|
|
themeIcons.forEach(icon => {
|
|
icon.className = newTheme === 'light' ? '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 = newTheme === 'light' ? 'Thème clair' : 'Thème sombre';
|
|
}
|
|
}
|
|
|
|
// Initialize theme from localStorage
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const savedTheme = localStorage.getItem('theme');
|
|
if (savedTheme === 'light') {
|
|
document.documentElement.setAttribute('data-theme', '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);
|
|
});
|
|
|
|
// ===== SIDEBAR TOGGLE =====
|
|
function toggleSidebar() {
|
|
const sb = document.getElementById('sidebar');
|
|
const icon = document.querySelector('#sidebar-toggle i');
|
|
const isCollapsed = sb.classList.toggle('collapsed');
|
|
|
|
if (isCollapsed) {
|
|
document.body.classList.add('sidebar-collapsed');
|
|
if(icon) icon.className = 'fas fa-angles-right';
|
|
localStorage.setItem('mc_sidebar', 'collapsed');
|
|
} else {
|
|
document.body.classList.remove('sidebar-collapsed');
|
|
if(icon) icon.className = 'fas fa-angles-left';
|
|
localStorage.setItem('mc_sidebar', 'expanded');
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
if (localStorage.getItem('mc_sidebar') === 'collapsed') {
|
|
document.getElementById('sidebar')?.classList.add('collapsed');
|
|
document.body.classList.add('sidebar-collapsed');
|
|
const icon = document.querySelector('#sidebar-toggle i');
|
|
if(icon) icon.className = 'fas fa-angles-right';
|
|
}
|
|
});
|
|
|
|
// ===== COMMAND PALETTE =====
|
|
let cmdOpen = false;
|
|
function openCmdPalette() {
|
|
cmdOpen = true;
|
|
document.getElementById('cmd-palette').classList.add('open');
|
|
const input = document.getElementById('cmd-input');
|
|
if(input) {
|
|
input.value = '';
|
|
input.focus();
|
|
}
|
|
// Add anime.js animation if loaded
|
|
if(window.anime) {
|
|
anime({ targets:'#cmd-box', scale:[.95,1], opacity:[0,1], duration:200, easing:'easeOutCubic' });
|
|
}
|
|
}
|
|
function closeCmdPalette() {
|
|
cmdOpen = false;
|
|
if(window.anime) {
|
|
anime({ targets:'#cmd-box', scale:.95, opacity:0, duration:150, easing:'easeInCubic', complete:()=>document.getElementById('cmd-palette').classList.remove('open') });
|
|
} else {
|
|
document.getElementById('cmd-palette').classList.remove('open');
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', e => {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
e.preventDefault();
|
|
cmdOpen ? closeCmdPalette() : openCmdPalette();
|
|
}
|
|
if (e.key === 'Escape') {
|
|
if (cmdOpen) closeCmdPalette();
|
|
}
|
|
});
|
|
|
|
// ===== 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 = false; // Set to true only for targeted mobile diagnostics
|
|
|
|
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 });
|
|
}
|
|
})();
|
|
|
|
// ===== DROPDOWN HANDLER (MOBILE & DESKTOP) =====
|
|
// Uses click-based dropdowns on all devices for consistency
|
|
// EVENT DELEGATION handles dynamically created elements
|
|
(function() {
|
|
const dropdownDebug = false;
|
|
const ddLog = (...args) => { if (dropdownDebug) console.log(...args); };
|
|
ddLog('[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');
|
|
|
|
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');
|
|
ddLog('[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');
|
|
ddLog('[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()) {
|
|
ddLog('[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) {
|
|
ddLog('[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 });
|
|
|
|
ddLog('[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 {
|
|
// Direct API call to see the exact response
|
|
const apiBase = window.location.origin;
|
|
const payload = {
|
|
username,
|
|
password,
|
|
email: email || null,
|
|
display_name: displayName || null
|
|
};
|
|
console.log('[Setup] Sending setup request:', JSON.stringify(payload));
|
|
|
|
const response = await fetch(`${apiBase}/api/auth/setup`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const responseBody = await response.text();
|
|
console.log('[Setup] Response status:', response.status);
|
|
console.log('[Setup] Response body:', responseBody);
|
|
|
|
if (!response.ok) {
|
|
let errorMessage = 'Échec de configuration';
|
|
try {
|
|
const errorData = JSON.parse(responseBody);
|
|
if (errorData.detail) {
|
|
if (Array.isArray(errorData.detail)) {
|
|
errorMessage = errorData.detail.map(err => err.msg).join(', ');
|
|
} else {
|
|
errorMessage = errorData.detail;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
errorMessage = `Erreur serveur (${response.status}): ${responseBody.substring(0, 200)}`;
|
|
}
|
|
errorText.textContent = errorMessage;
|
|
errorEl.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
// Setup succeeded, now login
|
|
console.log('[Setup] Setup successful, attempting login...');
|
|
const loginResult = await dashboard.login(username, password);
|
|
console.log('[Setup] Login result:', loginResult);
|
|
|
|
if (!loginResult) {
|
|
// Login failed but setup succeeded — show success and redirect to login
|
|
errorEl.classList.remove('hidden');
|
|
errorEl.querySelector('i').className = 'fas fa-check-circle mr-2';
|
|
errorEl.classList.remove('text-red-400', 'bg-red-900/20', 'border-red-800');
|
|
errorEl.classList.add('text-green-400', 'bg-green-900/20', 'border-green-800');
|
|
errorText.textContent = 'Compte créé avec succès ! Redirection vers la connexion...';
|
|
setTimeout(() => { window.location.reload(); }, 2000);
|
|
}
|
|
} catch (error) {
|
|
console.error('[Setup] Error:', error);
|
|
const msg = error.message || String(error) || 'Erreur inconnue lors de la création du compte';
|
|
errorText.textContent = msg;
|
|
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>
|
|
|
|
<script src="https://code.iconify.design/3/3.1.1/iconify.min.js"></script>
|
|
|
|
<script src="/static/icon_picker.js"></script>
|
|
<script src="/static/favorites_manager.js"></script>
|
|
<script src="/static/container_customizations_manager.js"></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>
|
|
|
|
<!-- PWA Service Worker Registration -->
|
|
<script>
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('/sw.js').then(function(reg) {
|
|
console.log('SW registered:', reg.scope);
|
|
}).catch(function(err) {
|
|
console.warn('SW registration failed:', err);
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |