| .. | .. |
|---|
| 1 | 1 | <!DOCTYPE html> |
|---|
| 2 | | -<html lang="en" class="h-full"> |
|---|
| 2 | +<html lang="en"> |
|---|
| 3 | 3 | <head> |
|---|
| 4 | | - <meta charset="UTF-8" /> |
|---|
| 5 | | - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|---|
| 4 | + <meta charset="UTF-8"> |
|---|
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|---|
| 6 | 6 | <title>OPS Dashboard</title> |
|---|
| 7 | | - |
|---|
| 8 | | - <!-- Tailwind CSS Play CDN --> |
|---|
| 9 | 7 | <script src="https://cdn.tailwindcss.com"></script> |
|---|
| 10 | | - <script> |
|---|
| 11 | | - tailwind.config = { |
|---|
| 12 | | - theme: { |
|---|
| 13 | | - extend: { |
|---|
| 14 | | - colors: { |
|---|
| 15 | | - gray: { |
|---|
| 16 | | - 950: '#0a0e1a' |
|---|
| 17 | | - } |
|---|
| 18 | | - } |
|---|
| 19 | | - } |
|---|
| 20 | | - } |
|---|
| 21 | | - }; |
|---|
| 22 | | - </script> |
|---|
| 23 | | - |
|---|
| 24 | | - <!-- Custom styles --> |
|---|
| 25 | | - <link rel="stylesheet" href="/static/css/style.css" /> |
|---|
| 26 | | - |
|---|
| 27 | | - <!-- Alpine.js v3 --> |
|---|
| 28 | | - <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> |
|---|
| 29 | | - |
|---|
| 30 | | - <!-- App logic --> |
|---|
| 31 | | - <script src="/static/js/app.js"></script> |
|---|
| 8 | + <link rel="stylesheet" href="/static/css/style.css"> |
|---|
| 9 | + <style> |
|---|
| 10 | + body { background: #0f172a; color: #e2e8f0; margin: 0; } |
|---|
| 11 | + #app { display: flex; min-height: 100vh; } |
|---|
| 12 | + #sidebar { width: 240px; background: #111827; border-right: 1px solid #1f2937; display: flex; flex-direction: column; flex-shrink: 0; } |
|---|
| 13 | + #main { flex: 1; display: flex; flex-direction: column; overflow-x: hidden; } |
|---|
| 14 | + #topbar { background: #111827; border-bottom: 1px solid #1f2937; padding: 0.75rem 1.5rem; display: flex; align-items: center; gap: 1rem; } |
|---|
| 15 | + #page-content { flex: 1; padding: 1.5rem; overflow-y: auto; } |
|---|
| 16 | + .breadcrumb { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; color: #9ca3af; } |
|---|
| 17 | + .breadcrumb a { color: #60a5fa; cursor: pointer; text-decoration: none; } |
|---|
| 18 | + .breadcrumb a:hover { text-decoration: underline; } |
|---|
| 19 | + .breadcrumb .sep { color: #4b5563; } |
|---|
| 20 | + .breadcrumb .current { color: #e2e8f0; font-weight: 500; } |
|---|
| 21 | + .hamburger { display: none; background: none; border: none; color: #9ca3af; font-size: 1.5rem; cursor: pointer; padding: 0.25rem; } |
|---|
| 22 | + .sidebar-logo { padding: 1.25rem 1rem; font-size: 1.125rem; font-weight: 700; color: #f3f4f6; border-bottom: 1px solid #1f2937; display: flex; align-items: center; gap: 0.5rem; } |
|---|
| 23 | + .sidebar-nav { padding: 0.75rem 0.5rem; flex: 1; } |
|---|
| 24 | + .sidebar-footer { padding: 0.75rem 1rem; border-top: 1px solid #1f2937; font-size: 0.75rem; color: #6b7280; } |
|---|
| 25 | + .project-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; } |
|---|
| 26 | + .env-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1rem; } |
|---|
| 27 | + .service-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; } |
|---|
| 28 | + .stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; } |
|---|
| 29 | + .card-clickable { cursor: pointer; transition: border-color 0.2s, transform 0.15s; } |
|---|
| 30 | + .card-clickable:hover { border-color: #60a5fa; transform: translateY(-1px); } |
|---|
| 31 | + .mobile-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 40; } |
|---|
| 32 | + @media (max-width: 768px) { |
|---|
| 33 | + #sidebar { position: fixed; left: -240px; top: 0; bottom: 0; z-index: 50; transition: left 0.2s; } |
|---|
| 34 | + #sidebar.open { left: 0; } |
|---|
| 35 | + .mobile-overlay.open { display: block; } |
|---|
| 36 | + .hamburger { display: block; } |
|---|
| 37 | + .project-grid, .env-grid, .service-grid { grid-template-columns: 1fr; } |
|---|
| 38 | + } |
|---|
| 39 | + </style> |
|---|
| 32 | 40 | </head> |
|---|
| 41 | +<body> |
|---|
| 33 | 42 | |
|---|
| 34 | | -<body class="h-full bg-gray-900 text-gray-100 antialiased"> |
|---|
| 35 | | - |
|---|
| 36 | | -<!-- ============================================================ |
|---|
| 37 | | - Toast Notifications |
|---|
| 38 | | - ============================================================ --> |
|---|
| 39 | | -<div class="toast-container" x-data x-show="$store.toast.toasts.length > 0"> |
|---|
| 40 | | - <template x-for="t in $store.toast.toasts" :key="t.id"> |
|---|
| 41 | | - <div class="toast" :class="t.type"> |
|---|
| 42 | | - <span class="text-base leading-none" x-text="$store.toast.iconFor(t.type)"></span> |
|---|
| 43 | | - <span x-text="t.msg" class="flex-1"></span> |
|---|
| 44 | | - <button class="toast-dismiss" @click="$store.toast.remove(t.id)">✕</button> |
|---|
| 45 | | - </div> |
|---|
| 46 | | - </template> |
|---|
| 47 | | -</div> |
|---|
| 48 | | - |
|---|
| 49 | | -<!-- ============================================================ |
|---|
| 50 | | - Login Screen |
|---|
| 51 | | - ============================================================ --> |
|---|
| 52 | | -<div x-data x-show="!$store.auth.isAuthenticated" class="flex h-full items-center justify-center"> |
|---|
| 53 | | - <div class="w-full max-w-sm px-4"> |
|---|
| 54 | | - <div class="card text-center"> |
|---|
| 55 | | - <!-- Server icon --> |
|---|
| 56 | | - <div class="flex justify-center mb-4"> |
|---|
| 57 | | - <div class="bg-blue-600 rounded-xl p-3"> |
|---|
| 58 | | - <svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> |
|---|
| 59 | | - <path stroke-linecap="round" stroke-linejoin="round" |
|---|
| 60 | | - d="M21.75 17.25v.75A2.25 2.25 0 0119.5 20.25h-15a2.25 2.25 0 01-2.25-2.25v-.75m19.5 0A2.25 2.25 0 0019.5 15h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v-3A2.25 2.25 0 0019.5 9.75h-15A2.25 2.25 0 002.25 12v3M12 12.75h.008v.008H12v-.008zM12 6.75h.008v.008H12V6.75z"/> |
|---|
| 61 | | - </svg> |
|---|
| 62 | | - </div> |
|---|
| 63 | | - </div> |
|---|
| 64 | | - |
|---|
| 65 | | - <h1 class="text-2xl font-bold text-white mb-1">OPS Dashboard</h1> |
|---|
| 66 | | - <p class="text-sm text-gray-500 mb-6">Enter your access token to continue</p> |
|---|
| 67 | | - |
|---|
| 68 | | - <form @submit.prevent="$store.auth.login()" class="space-y-4"> |
|---|
| 69 | | - <div> |
|---|
| 70 | | - <input |
|---|
| 71 | | - type="password" |
|---|
| 72 | | - class="form-input text-center tracking-widest" |
|---|
| 73 | | - placeholder="••••••••••••••••" |
|---|
| 74 | | - x-model="$store.auth.loginInput" |
|---|
| 75 | | - autofocus |
|---|
| 76 | | - /> |
|---|
| 77 | | - </div> |
|---|
| 78 | | - |
|---|
| 79 | | - <div x-show="$store.auth.loginError" class="text-red-400 text-sm" x-text="$store.auth.loginError"></div> |
|---|
| 80 | | - |
|---|
| 81 | | - <button type="submit" class="btn btn-primary w-full" :disabled="$store.auth.loading"> |
|---|
| 82 | | - <span x-show="$store.auth.loading" class="spinner"></span> |
|---|
| 83 | | - <span x-text="$store.auth.loading ? 'Verifying…' : 'Sign In'"></span> |
|---|
| 84 | | - </button> |
|---|
| 85 | | - </form> |
|---|
| 86 | | - </div> |
|---|
| 43 | +<!-- Login Overlay --> |
|---|
| 44 | +<div id="login-overlay" style="position:fixed;inset:0;background:#0f172a;z-index:100;display:flex;align-items:center;justify-content:center;"> |
|---|
| 45 | + <div class="card" style="width:100%;max-width:380px;text-align:center;"> |
|---|
| 46 | + <div style="font-size:1.5rem;font-weight:700;margin-bottom:1.5rem;color:#f3f4f6;">OPS Dashboard</div> |
|---|
| 47 | + <input type="password" id="login-token" placeholder="Enter access token" class="form-input" style="margin-bottom:1rem;" |
|---|
| 48 | + onkeydown="if(event.key==='Enter')doLogin()"> |
|---|
| 49 | + <button onclick="doLogin()" class="btn btn-primary" style="width:100%;">Login</button> |
|---|
| 50 | + <div id="login-error" style="color:#f87171;font-size:0.875rem;margin-top:0.75rem;display:none;"></div> |
|---|
| 87 | 51 | </div> |
|---|
| 88 | 52 | </div> |
|---|
| 89 | 53 | |
|---|
| 90 | | -<!-- ============================================================ |
|---|
| 91 | | - Main App Shell |
|---|
| 92 | | - ============================================================ --> |
|---|
| 93 | | -<div x-data x-show="$store.auth.isAuthenticated" class="flex h-full"> |
|---|
| 54 | +<!-- App Shell --> |
|---|
| 55 | +<div id="app" style="display:none;"> |
|---|
| 56 | + <!-- Mobile overlay --> |
|---|
| 57 | + <div id="mobile-overlay" class="mobile-overlay" onclick="toggleSidebar()"></div> |
|---|
| 94 | 58 | |
|---|
| 95 | | - <!-- Mobile sidebar overlay --> |
|---|
| 96 | | - <div |
|---|
| 97 | | - x-show="$store.app.sidebarOpen" |
|---|
| 98 | | - class="sidebar-mobile-overlay md:hidden" |
|---|
| 99 | | - @click="$store.app.sidebarOpen = false" |
|---|
| 100 | | - ></div> |
|---|
| 101 | | - |
|---|
| 102 | | - <!-- ---- Sidebar ---- --> |
|---|
| 103 | | - <aside |
|---|
| 104 | | - class="fixed inset-y-0 left-0 z-50 flex flex-col w-60 bg-gray-950 border-r border-gray-800 transform transition-transform duration-200 md:relative md:translate-x-0" |
|---|
| 105 | | - :class="$store.app.sidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'" |
|---|
| 106 | | - > |
|---|
| 107 | | - <!-- App name --> |
|---|
| 108 | | - <div class="flex items-center gap-3 px-4 py-5 border-b border-gray-800"> |
|---|
| 109 | | - <div class="bg-blue-600 rounded-lg p-1.5 flex-shrink-0"> |
|---|
| 110 | | - <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> |
|---|
| 111 | | - <path stroke-linecap="round" stroke-linejoin="round" |
|---|
| 112 | | - d="M21.75 17.25v.75A2.25 2.25 0 0119.5 20.25h-15A2.25 2.25 0 012.25 18v-.75m19.5 0A2.25 2.25 0 0019.5 15h-15A2.25 2.25 0 002.25 17.25m19.5 0v-3A2.25 2.25 0 0019.5 9.75h-15A2.25 2.25 0 002.25 12v3M12 12.75h.008v.008H12v-.008zM12 6.75h.008v.008H12V6.75z"/> |
|---|
| 113 | | - </svg> |
|---|
| 114 | | - </div> |
|---|
| 115 | | - <div> |
|---|
| 116 | | - <div class="font-bold text-white text-base tracking-widest">OPS</div> |
|---|
| 117 | | - <div class="text-xs text-gray-500">tekmidian.com</div> |
|---|
| 118 | | - </div> |
|---|
| 59 | + <!-- Sidebar --> |
|---|
| 60 | + <aside id="sidebar"> |
|---|
| 61 | + <div class="sidebar-logo"> |
|---|
| 62 | + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#60a5fa" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1" fill="#60a5fa"/><circle cx="6" cy="18" r="1" fill="#60a5fa"/></svg> |
|---|
| 63 | + OPS Dashboard |
|---|
| 119 | 64 | </div> |
|---|
| 120 | | - |
|---|
| 121 | | - <!-- Nav links --> |
|---|
| 122 | | - <nav class="flex-1 p-3 space-y-1 overflow-y-auto"> |
|---|
| 123 | | - |
|---|
| 124 | | - <button class="sidebar-link w-full text-left" |
|---|
| 125 | | - :class="$store.app.page === 'dashboard' ? 'active' : ''" |
|---|
| 126 | | - @click="$store.app.navigate('dashboard')"> |
|---|
| 127 | | - <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> |
|---|
| 128 | | - <path stroke-linecap="round" stroke-linejoin="round" |
|---|
| 129 | | - d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"/> |
|---|
| 130 | | - </svg> |
|---|
| 65 | + <nav class="sidebar-nav" id="sidebar-nav"> |
|---|
| 66 | + <a class="sidebar-link active" data-page="dashboard" onclick="showPage('dashboard')"> |
|---|
| 67 | + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg> |
|---|
| 131 | 68 | Dashboard |
|---|
| 132 | | - </button> |
|---|
| 133 | | - |
|---|
| 134 | | - <button class="sidebar-link w-full text-left" |
|---|
| 135 | | - :class="$store.app.page === 'backups' ? 'active' : ''" |
|---|
| 136 | | - @click="$store.app.navigate('backups')"> |
|---|
| 137 | | - <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> |
|---|
| 138 | | - <path stroke-linecap="round" stroke-linejoin="round" |
|---|
| 139 | | - d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"/> |
|---|
| 140 | | - </svg> |
|---|
| 141 | | - Backups |
|---|
| 142 | | - </button> |
|---|
| 143 | | - |
|---|
| 144 | | - <button class="sidebar-link w-full text-left" |
|---|
| 145 | | - :class="$store.app.page === 'restore' ? 'active' : ''" |
|---|
| 146 | | - @click="$store.app.navigate('restore')"> |
|---|
| 147 | | - <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> |
|---|
| 148 | | - <path stroke-linecap="round" stroke-linejoin="round" |
|---|
| 149 | | - d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3"/> |
|---|
| 150 | | - </svg> |
|---|
| 151 | | - Restore |
|---|
| 152 | | - </button> |
|---|
| 153 | | - |
|---|
| 154 | | - <button class="sidebar-link w-full text-left" |
|---|
| 155 | | - :class="$store.app.page === 'services' ? 'active' : ''" |
|---|
| 156 | | - @click="$store.app.navigate('services')"> |
|---|
| 157 | | - <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> |
|---|
| 158 | | - <path stroke-linecap="round" stroke-linejoin="round" |
|---|
| 159 | | - d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z"/> |
|---|
| 160 | | - </svg> |
|---|
| 69 | + </a> |
|---|
| 70 | + <a class="sidebar-link" data-page="services" onclick="showPage('services')"> |
|---|
| 71 | + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg> |
|---|
| 161 | 72 | Services |
|---|
| 162 | | - </button> |
|---|
| 163 | | - |
|---|
| 164 | | - <button class="sidebar-link w-full text-left" |
|---|
| 165 | | - :class="$store.app.page === 'system' ? 'active' : ''" |
|---|
| 166 | | - @click="$store.app.navigate('system')"> |
|---|
| 167 | | - <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> |
|---|
| 168 | | - <path stroke-linecap="round" stroke-linejoin="round" |
|---|
| 169 | | - d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23-.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5"/> |
|---|
| 170 | | - </svg> |
|---|
| 73 | + </a> |
|---|
| 74 | + <a class="sidebar-link" data-page="backups" onclick="showPage('backups')"> |
|---|
| 75 | + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> |
|---|
| 76 | + Backups |
|---|
| 77 | + </a> |
|---|
| 78 | + <a class="sidebar-link" data-page="system" onclick="showPage('system')"> |
|---|
| 79 | + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg> |
|---|
| 171 | 80 | System |
|---|
| 172 | | - </button> |
|---|
| 81 | + </a> |
|---|
| 82 | + <a class="sidebar-link" data-page="restore" onclick="showPage('restore')"> |
|---|
| 83 | + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 102.13-9.36L1 10"/></svg> |
|---|
| 84 | + Restore |
|---|
| 85 | + </a> |
|---|
| 173 | 86 | </nav> |
|---|
| 174 | | - |
|---|
| 175 | | - <!-- Logout --> |
|---|
| 176 | | - <div class="p-3 border-t border-gray-800"> |
|---|
| 177 | | - <button class="sidebar-link w-full text-left text-red-400 hover:text-red-300" |
|---|
| 178 | | - @click="$store.auth.logout()"> |
|---|
| 179 | | - <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> |
|---|
| 180 | | - <path stroke-linecap="round" stroke-linejoin="round" |
|---|
| 181 | | - d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75"/> |
|---|
| 182 | | - </svg> |
|---|
| 183 | | - Sign Out |
|---|
| 184 | | - </button> |
|---|
| 87 | + <div class="sidebar-footer"> |
|---|
| 88 | + <a onclick="doLogout()" style="color:#9ca3af;cursor:pointer;">Logout</a> |
|---|
| 185 | 89 | </div> |
|---|
| 186 | 90 | </aside> |
|---|
| 187 | 91 | |
|---|
| 188 | | - <!-- ---- Main content ---- --> |
|---|
| 189 | | - <div class="flex-1 flex flex-col min-w-0 overflow-hidden"> |
|---|
| 190 | | - |
|---|
| 191 | | - <!-- Top header --> |
|---|
| 192 | | - <header class="flex items-center justify-between px-4 py-3 bg-gray-950 border-b border-gray-800 flex-shrink-0"> |
|---|
| 193 | | - |
|---|
| 194 | | - <!-- Mobile hamburger --> |
|---|
| 195 | | - <button class="md:hidden p-1 rounded text-gray-400 hover:text-white hover:bg-gray-800" |
|---|
| 196 | | - @click="$store.app.sidebarOpen = !$store.app.sidebarOpen"> |
|---|
| 197 | | - <svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> |
|---|
| 198 | | - <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/> |
|---|
| 199 | | - </svg> |
|---|
| 200 | | - </button> |
|---|
| 201 | | - |
|---|
| 202 | | - <!-- Page title --> |
|---|
| 203 | | - <h2 class="text-sm font-semibold text-gray-300 capitalize" |
|---|
| 204 | | - x-text="$store.app.page"></h2> |
|---|
| 205 | | - |
|---|
| 206 | | - <!-- Right side: refresh indicator --> |
|---|
| 207 | | - <div class="flex items-center gap-3"> |
|---|
| 208 | | - |
|---|
| 209 | | - <!-- Auto-refresh for dashboard --> |
|---|
| 210 | | - <div x-show="$store.app.page === 'dashboard'" class="flex items-center gap-2 text-xs text-gray-500"> |
|---|
| 211 | | - <button |
|---|
| 212 | | - @click="$store.dashboard.toggleAutoRefresh()" |
|---|
| 213 | | - class="flex items-center gap-1.5 hover:text-gray-300 transition-colors" |
|---|
| 214 | | - :title="$store.dashboard.autoRefreshEnabled ? 'Auto-refresh on (click to pause)' : 'Auto-refresh off (click to resume)'"> |
|---|
| 215 | | - <div class="refresh-ring" :class="!$store.dashboard.autoRefreshEnabled ? 'paused' : ''"></div> |
|---|
| 216 | | - <span x-show="$store.dashboard.lastRefresh" |
|---|
| 217 | | - x-text="'Updated ' + $store.dashboard.timeAgo($store.dashboard.lastRefresh)"></span> |
|---|
| 218 | | - </button> |
|---|
| 219 | | - |
|---|
| 220 | | - <button @click="$store.dashboard.manualRefresh()" |
|---|
| 221 | | - class="btn btn-ghost btn-xs" |
|---|
| 222 | | - :disabled="$store.dashboard.loading"> |
|---|
| 223 | | - <svg class="w-3 h-3" :class="$store.dashboard.refreshing ? 'animate-spin' : ''" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
|---|
| 224 | | - <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-4.566l3.181 3.182m0-4.991v4.99"/> |
|---|
| 225 | | - </svg> |
|---|
| 226 | | - Refresh |
|---|
| 227 | | - </button> |
|---|
| 228 | | - </div> |
|---|
| 229 | | - |
|---|
| 92 | + <!-- Main Content --> |
|---|
| 93 | + <div id="main"> |
|---|
| 94 | + <!-- Top bar --> |
|---|
| 95 | + <div id="topbar"> |
|---|
| 96 | + <button class="hamburger" onclick="toggleSidebar()">☰</button> |
|---|
| 97 | + <div id="breadcrumbs" class="breadcrumb" style="flex:1;"></div> |
|---|
| 98 | + <div style="display:flex;align-items:center;gap:0.75rem;"> |
|---|
| 99 | + <div id="refresh-indicator" class="refresh-ring paused" title="Auto-refresh"></div> |
|---|
| 100 | + <button class="btn btn-ghost btn-xs" onclick="refreshCurrentPage()" title="Refresh now">Refresh</button> |
|---|
| 230 | 101 | </div> |
|---|
| 231 | | - </header> |
|---|
| 102 | + </div> |
|---|
| 232 | 103 | |
|---|
| 233 | | - <!-- ---- Page content area ---- --> |
|---|
| 234 | | - <main class="flex-1 overflow-y-auto"> |
|---|
| 235 | | - |
|---|
| 236 | | - <!-- ====================================================== |
|---|
| 237 | | - DASHBOARD PAGE |
|---|
| 238 | | - ====================================================== --> |
|---|
| 239 | | - <div x-show="$store.app.page === 'dashboard'" class="p-4 md:p-6 page-enter"> |
|---|
| 240 | | - |
|---|
| 241 | | - <!-- Loading --> |
|---|
| 242 | | - <div x-show="$store.dashboard.loading && !$store.dashboard.projects.length" |
|---|
| 243 | | - class="flex items-center justify-center h-48 text-gray-500"> |
|---|
| 244 | | - <div class="text-center"> |
|---|
| 245 | | - <div class="spinner spinner-lg mx-auto mb-3"></div> |
|---|
| 246 | | - <div class="text-sm">Loading containers…</div> |
|---|
| 247 | | - </div> |
|---|
| 248 | | - </div> |
|---|
| 249 | | - |
|---|
| 250 | | - <!-- Error --> |
|---|
| 251 | | - <div x-show="$store.dashboard.error" class="card border-red-800 text-red-400 mb-4"> |
|---|
| 252 | | - <div class="flex items-center gap-2"> |
|---|
| 253 | | - <span class="text-lg">⚠</span> |
|---|
| 254 | | - <span x-text="$store.dashboard.error"></span> |
|---|
| 255 | | - </div> |
|---|
| 256 | | - </div> |
|---|
| 257 | | - |
|---|
| 258 | | - <!-- Projects --> |
|---|
| 259 | | - <template x-for="proj in $store.dashboard.projects" :key="proj.name"> |
|---|
| 260 | | - <div class="mb-8"> |
|---|
| 261 | | - <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-widest mb-3" |
|---|
| 262 | | - x-text="proj.name"></h3> |
|---|
| 263 | | - |
|---|
| 264 | | - <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3"> |
|---|
| 265 | | - <template x-for="svc in proj.services" :key="svc.name || svc.service"> |
|---|
| 266 | | - <div class="card flex flex-col gap-2"> |
|---|
| 267 | | - <!-- Name + badge --> |
|---|
| 268 | | - <div class="flex items-start justify-between gap-2"> |
|---|
| 269 | | - <div class="font-medium text-white text-sm truncate" |
|---|
| 270 | | - x-text="svc.name || svc.service"></div> |
|---|
| 271 | | - <div class="badge flex-shrink-0" |
|---|
| 272 | | - :class="$store.dashboard.badgeClass(svc.status, svc.health)"> |
|---|
| 273 | | - <span class="status-dot" |
|---|
| 274 | | - :class="$store.dashboard.dotClass(svc.status, svc.health)"></span> |
|---|
| 275 | | - <span x-text="svc.status || 'unknown'"></span> |
|---|
| 276 | | - </div> |
|---|
| 277 | | - </div> |
|---|
| 278 | | - |
|---|
| 279 | | - <!-- Health + uptime row --> |
|---|
| 280 | | - <div class="flex items-center gap-3 text-xs text-gray-500"> |
|---|
| 281 | | - <template x-if="svc.health && svc.health !== ''"> |
|---|
| 282 | | - <span class="flex items-center gap-1"> |
|---|
| 283 | | - <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
|---|
| 284 | | - <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/> |
|---|
| 285 | | - </svg> |
|---|
| 286 | | - <span x-text="svc.health"></span> |
|---|
| 287 | | - </span> |
|---|
| 288 | | - </template> |
|---|
| 289 | | - <template x-if="svc.uptime || svc.started"> |
|---|
| 290 | | - <span x-text="svc.uptime || ('Up ' + $store.dashboard.timeAgo(svc.started))"></span> |
|---|
| 291 | | - </template> |
|---|
| 292 | | - </div> |
|---|
| 293 | | - |
|---|
| 294 | | - <!-- Domain link --> |
|---|
| 295 | | - <template x-if="svc.domain"> |
|---|
| 296 | | - <a :href="'https://' + svc.domain" target="_blank" rel="noopener" |
|---|
| 297 | | - class="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1 truncate"> |
|---|
| 298 | | - <svg class="w-3 h-3 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
|---|
| 299 | | - <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/> |
|---|
| 300 | | - </svg> |
|---|
| 301 | | - <span x-text="svc.domain" class="truncate"></span> |
|---|
| 302 | | - </a> |
|---|
| 303 | | - </template> |
|---|
| 304 | | - </div> |
|---|
| 305 | | - </template> |
|---|
| 306 | | - </div> |
|---|
| 307 | | - </div> |
|---|
| 308 | | - </template> |
|---|
| 309 | | - |
|---|
| 310 | | - <!-- Empty state --> |
|---|
| 311 | | - <div x-show="!$store.dashboard.loading && !$store.dashboard.projects.length && !$store.dashboard.error" |
|---|
| 312 | | - class="flex flex-col items-center justify-center h-48 text-gray-600"> |
|---|
| 313 | | - <svg class="w-10 h-10 mb-3" fill="none" stroke="currentColor" stroke-width="1" viewBox="0 0 24 24"> |
|---|
| 314 | | - <path stroke-linecap="round" stroke-linejoin="round" |
|---|
| 315 | | - d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7"/> |
|---|
| 316 | | - </svg> |
|---|
| 317 | | - <p class="text-sm">No containers found</p> |
|---|
| 318 | | - </div> |
|---|
| 319 | | - </div> |
|---|
| 320 | | - |
|---|
| 321 | | - <!-- ====================================================== |
|---|
| 322 | | - BACKUPS PAGE |
|---|
| 323 | | - ====================================================== --> |
|---|
| 324 | | - <div x-show="$store.app.page === 'backups'" class="p-4 md:p-6 page-enter space-y-8"> |
|---|
| 325 | | - |
|---|
| 326 | | - <!-- ---- Local Backups ---- --> |
|---|
| 327 | | - <section> |
|---|
| 328 | | - <div class="flex items-center justify-between mb-4"> |
|---|
| 329 | | - <h3 class="text-sm font-semibold text-gray-400 uppercase tracking-widest">Local Backups</h3> |
|---|
| 330 | | - <button class="btn btn-ghost btn-sm" @click="$store.backups.fetchLocal()" |
|---|
| 331 | | - :disabled="$store.backups.loading"> |
|---|
| 332 | | - <span x-show="$store.backups.loading" class="spinner"></span> |
|---|
| 333 | | - Refresh |
|---|
| 334 | | - </button> |
|---|
| 335 | | - </div> |
|---|
| 336 | | - |
|---|
| 337 | | - <div x-show="$store.backups.loading && !$store.backups.local.length" |
|---|
| 338 | | - class="flex items-center gap-2 text-gray-500 text-sm py-6 justify-center"> |
|---|
| 339 | | - <div class="spinner"></div> Loading… |
|---|
| 340 | | - </div> |
|---|
| 341 | | - |
|---|
| 342 | | - <div x-show="!$store.backups.loading && !$store.backups.local.length" |
|---|
| 343 | | - class="text-gray-600 text-sm py-6 text-center">No local backups found</div> |
|---|
| 344 | | - |
|---|
| 345 | | - <div x-show="$store.backups.local.length > 0" class="table-wrapper"> |
|---|
| 346 | | - <table class="ops-table"> |
|---|
| 347 | | - <thead> |
|---|
| 348 | | - <tr> |
|---|
| 349 | | - <th>Project</th> |
|---|
| 350 | | - <th>Env</th> |
|---|
| 351 | | - <th>File</th> |
|---|
| 352 | | - <th>Size</th> |
|---|
| 353 | | - <th>Age</th> |
|---|
| 354 | | - <th>Actions</th> |
|---|
| 355 | | - </tr> |
|---|
| 356 | | - </thead> |
|---|
| 357 | | - <tbody> |
|---|
| 358 | | - <template x-for="(b, idx) in $store.backups.local" :key="idx"> |
|---|
| 359 | | - <tr> |
|---|
| 360 | | - <td class="font-medium text-white" x-text="b.project || '—'"></td> |
|---|
| 361 | | - <td> |
|---|
| 362 | | - <span class="badge badge-blue" x-text="b.env || '—'"></span> |
|---|
| 363 | | - </td> |
|---|
| 364 | | - <td class="mono text-xs text-gray-400 max-w-xs truncate" x-text="b.filename || b.file || '—'"></td> |
|---|
| 365 | | - <td class="text-gray-400 text-xs" x-text="$store.backups.formatBytes(b.size)"></td> |
|---|
| 366 | | - <td> |
|---|
| 367 | | - <span class="badge" |
|---|
| 368 | | - :class="$store.backups.ageBadge(b.created_at || b.modified)" |
|---|
| 369 | | - x-text="$store.backups.timeAgo(b.created_at || b.modified)"></span> |
|---|
| 370 | | - </td> |
|---|
| 371 | | - <td> |
|---|
| 372 | | - <div class="flex items-center gap-2"> |
|---|
| 373 | | - <button class="btn btn-success btn-xs" |
|---|
| 374 | | - @click="$store.backups.backupNow(b.project, b.env)" |
|---|
| 375 | | - :disabled="$store.backups.isRunning(b.project, b.env, 'backup')"> |
|---|
| 376 | | - <span x-show="$store.backups.isRunning(b.project, b.env, 'backup')" class="spinner"></span> |
|---|
| 377 | | - <span x-text="$store.backups.isRunning(b.project, b.env, 'backup') ? 'Running…' : 'Backup Now'"></span> |
|---|
| 378 | | - </button> |
|---|
| 379 | | - <button class="btn btn-ghost btn-xs" |
|---|
| 380 | | - @click="$store.backups.uploadOffsite(b.project, b.env)" |
|---|
| 381 | | - :disabled="$store.backups.isRunning(b.project, b.env, 'upload')"> |
|---|
| 382 | | - <span x-show="$store.backups.isRunning(b.project, b.env, 'upload')" class="spinner"></span> |
|---|
| 383 | | - <span x-text="$store.backups.isRunning(b.project, b.env, 'upload') ? 'Uploading…' : 'Upload Offsite'"></span> |
|---|
| 384 | | - </button> |
|---|
| 385 | | - </div> |
|---|
| 386 | | - </td> |
|---|
| 387 | | - </tr> |
|---|
| 388 | | - </template> |
|---|
| 389 | | - </tbody> |
|---|
| 390 | | - </table> |
|---|
| 391 | | - </div> |
|---|
| 392 | | - </section> |
|---|
| 393 | | - |
|---|
| 394 | | - <!-- ---- Offsite Backups ---- --> |
|---|
| 395 | | - <section> |
|---|
| 396 | | - <div class="flex items-center justify-between mb-4"> |
|---|
| 397 | | - <h3 class="text-sm font-semibold text-gray-400 uppercase tracking-widest">Offsite Backups</h3> |
|---|
| 398 | | - <div class="flex items-center gap-2"> |
|---|
| 399 | | - <button class="btn btn-warning btn-sm" |
|---|
| 400 | | - @click="$store.backups.applyRetention()" |
|---|
| 401 | | - :disabled="$store.backups.retentionRunning"> |
|---|
| 402 | | - <span x-show="$store.backups.retentionRunning" class="spinner"></span> |
|---|
| 403 | | - Apply Retention |
|---|
| 404 | | - </button> |
|---|
| 405 | | - <button class="btn btn-ghost btn-sm" @click="$store.backups.fetchOffsite()" |
|---|
| 406 | | - :disabled="$store.backups.loadingOffsite"> |
|---|
| 407 | | - <span x-show="$store.backups.loadingOffsite" class="spinner"></span> |
|---|
| 408 | | - Refresh |
|---|
| 409 | | - </button> |
|---|
| 410 | | - </div> |
|---|
| 411 | | - </div> |
|---|
| 412 | | - |
|---|
| 413 | | - <div x-show="$store.backups.loadingOffsite && !$store.backups.offsite.length" |
|---|
| 414 | | - class="flex items-center gap-2 text-gray-500 text-sm py-6 justify-center"> |
|---|
| 415 | | - <div class="spinner"></div> Loading… |
|---|
| 416 | | - </div> |
|---|
| 417 | | - |
|---|
| 418 | | - <div x-show="!$store.backups.loadingOffsite && !$store.backups.offsite.length" |
|---|
| 419 | | - class="text-gray-600 text-sm py-6 text-center">No offsite backups found</div> |
|---|
| 420 | | - |
|---|
| 421 | | - <div x-show="$store.backups.offsite.length > 0" class="table-wrapper"> |
|---|
| 422 | | - <table class="ops-table"> |
|---|
| 423 | | - <thead> |
|---|
| 424 | | - <tr> |
|---|
| 425 | | - <th>Project</th> |
|---|
| 426 | | - <th>Env</th> |
|---|
| 427 | | - <th>File</th> |
|---|
| 428 | | - <th>Size</th> |
|---|
| 429 | | - <th>Age</th> |
|---|
| 430 | | - </tr> |
|---|
| 431 | | - </thead> |
|---|
| 432 | | - <tbody> |
|---|
| 433 | | - <template x-for="(b, idx) in $store.backups.offsite" :key="idx"> |
|---|
| 434 | | - <tr> |
|---|
| 435 | | - <td class="font-medium text-white" x-text="b.project || '—'"></td> |
|---|
| 436 | | - <td> |
|---|
| 437 | | - <span class="badge badge-blue" x-text="b.env || '—'"></span> |
|---|
| 438 | | - </td> |
|---|
| 439 | | - <td class="mono text-xs text-gray-400 max-w-xs truncate" x-text="b.filename || b.file || '—'"></td> |
|---|
| 440 | | - <td class="text-gray-400 text-xs" x-text="$store.backups.formatBytes(b.size)"></td> |
|---|
| 441 | | - <td> |
|---|
| 442 | | - <span class="badge" |
|---|
| 443 | | - :class="$store.backups.ageBadge(b.created_at || b.modified)" |
|---|
| 444 | | - x-text="$store.backups.timeAgo(b.created_at || b.modified)"></span> |
|---|
| 445 | | - </td> |
|---|
| 446 | | - </tr> |
|---|
| 447 | | - </template> |
|---|
| 448 | | - </tbody> |
|---|
| 449 | | - </table> |
|---|
| 450 | | - </div> |
|---|
| 451 | | - </section> |
|---|
| 452 | | - </div> |
|---|
| 453 | | - |
|---|
| 454 | | - <!-- ====================================================== |
|---|
| 455 | | - RESTORE PAGE |
|---|
| 456 | | - ====================================================== --> |
|---|
| 457 | | - <div x-show="$store.app.page === 'restore'" class="p-4 md:p-6 page-enter"> |
|---|
| 458 | | - <div class="max-w-2xl"> |
|---|
| 459 | | - |
|---|
| 460 | | - <!-- Config card --> |
|---|
| 461 | | - <div class="card mb-6"> |
|---|
| 462 | | - <h3 class="text-sm font-semibold text-gray-300 mb-5">Restore Configuration</h3> |
|---|
| 463 | | - |
|---|
| 464 | | - <div class="grid grid-cols-1 sm:grid-cols-2 gap-5"> |
|---|
| 465 | | - |
|---|
| 466 | | - <!-- Source --> |
|---|
| 467 | | - <div> |
|---|
| 468 | | - <label class="form-label">Source</label> |
|---|
| 469 | | - <div class="flex gap-4 mt-1"> |
|---|
| 470 | | - <label class="flex items-center gap-2 cursor-pointer"> |
|---|
| 471 | | - <input type="radio" name="restore-source" value="local" |
|---|
| 472 | | - x-model="$store.restore.source" |
|---|
| 473 | | - @change="$store.restore.onSourceChange()" |
|---|
| 474 | | - class="accent-blue-500 w-4 h-4" /> |
|---|
| 475 | | - <span class="text-sm text-gray-300">Local</span> |
|---|
| 476 | | - </label> |
|---|
| 477 | | - <label class="flex items-center gap-2 cursor-pointer"> |
|---|
| 478 | | - <input type="radio" name="restore-source" value="offsite" |
|---|
| 479 | | - x-model="$store.restore.source" |
|---|
| 480 | | - @change="$store.restore.onSourceChange()" |
|---|
| 481 | | - class="accent-blue-500 w-4 h-4" /> |
|---|
| 482 | | - <span class="text-sm text-gray-300">Offsite</span> |
|---|
| 483 | | - </label> |
|---|
| 484 | | - </div> |
|---|
| 485 | | - </div> |
|---|
| 486 | | - |
|---|
| 487 | | - <!-- Dry run --> |
|---|
| 488 | | - <div class="flex items-center"> |
|---|
| 489 | | - <label class="flex items-center gap-2 cursor-pointer mt-5 sm:mt-0"> |
|---|
| 490 | | - <input type="checkbox" x-model="$store.restore.dryRun" |
|---|
| 491 | | - class="accent-blue-500 w-4 h-4 rounded" /> |
|---|
| 492 | | - <span class="text-sm text-gray-300">Dry run (simulate only)</span> |
|---|
| 493 | | - </label> |
|---|
| 494 | | - </div> |
|---|
| 495 | | - |
|---|
| 496 | | - <!-- Project --> |
|---|
| 497 | | - <div> |
|---|
| 498 | | - <label class="form-label">Project</label> |
|---|
| 499 | | - <div x-show="$store.restore.loadingProjects" class="flex items-center gap-2 text-gray-500 text-sm mt-1"> |
|---|
| 500 | | - <div class="spinner"></div> Loading… |
|---|
| 501 | | - </div> |
|---|
| 502 | | - <select class="form-select" x-show="!$store.restore.loadingProjects" |
|---|
| 503 | | - x-model="$store.restore.project" |
|---|
| 504 | | - @change="$store.restore.onProjectChange()"> |
|---|
| 505 | | - <option value="" disabled>Select project</option> |
|---|
| 506 | | - <template x-for="p in $store.restore.projects" :key="p"> |
|---|
| 507 | | - <option :value="p" x-text="p"></option> |
|---|
| 508 | | - </template> |
|---|
| 509 | | - </select> |
|---|
| 510 | | - </div> |
|---|
| 511 | | - |
|---|
| 512 | | - <!-- Environment --> |
|---|
| 513 | | - <div> |
|---|
| 514 | | - <label class="form-label">Environment</label> |
|---|
| 515 | | - <select class="form-select" x-model="$store.restore.env" |
|---|
| 516 | | - :disabled="!$store.restore.project"> |
|---|
| 517 | | - <option value="" disabled>Select environment</option> |
|---|
| 518 | | - <template x-for="e in $store.restore.envs" :key="e"> |
|---|
| 519 | | - <option :value="e" x-text="e"></option> |
|---|
| 520 | | - </template> |
|---|
| 521 | | - </select> |
|---|
| 522 | | - </div> |
|---|
| 523 | | - </div> |
|---|
| 524 | | - |
|---|
| 525 | | - <!-- Action bar --> |
|---|
| 526 | | - <div class="flex items-center gap-3 mt-6 pt-5 border-t border-gray-700"> |
|---|
| 527 | | - <button class="btn btn-danger" |
|---|
| 528 | | - @click="$store.restore.confirm()" |
|---|
| 529 | | - :disabled="$store.restore.running || !$store.restore.project || !$store.restore.env"> |
|---|
| 530 | | - <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> |
|---|
| 531 | | - <path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3"/> |
|---|
| 532 | | - </svg> |
|---|
| 533 | | - <span x-text="$store.restore.dryRun ? 'Dry Run' : 'Restore'"></span> |
|---|
| 534 | | - </button> |
|---|
| 535 | | - <button x-show="$store.restore.running" class="btn btn-ghost" |
|---|
| 536 | | - @click="$store.restore.abort()">Abort</button> |
|---|
| 537 | | - <span x-show="$store.restore.running" class="flex items-center gap-2 text-sm text-yellow-400"> |
|---|
| 538 | | - <div class="spinner"></div> Running… |
|---|
| 539 | | - </span> |
|---|
| 540 | | - </div> |
|---|
| 541 | | - </div> |
|---|
| 542 | | - |
|---|
| 543 | | - <!-- Output terminal --> |
|---|
| 544 | | - <div x-show="$store.restore.output.length > 0"> |
|---|
| 545 | | - <div class="flex items-center justify-between mb-2"> |
|---|
| 546 | | - <span class="text-xs text-gray-500 font-semibold uppercase tracking-widest">Output</span> |
|---|
| 547 | | - <button class="btn btn-ghost btn-xs" @click="$store.restore.output = []">Clear</button> |
|---|
| 548 | | - </div> |
|---|
| 549 | | - <div id="restore-output" class="terminal" style="height: 360px;"> |
|---|
| 550 | | - <template x-for="(line, i) in $store.restore.output" :key="i"> |
|---|
| 551 | | - <div :class="line.cls" x-text="line.text"></div> |
|---|
| 552 | | - </template> |
|---|
| 553 | | - </div> |
|---|
| 554 | | - </div> |
|---|
| 555 | | - |
|---|
| 556 | | - <!-- Error --> |
|---|
| 557 | | - <div x-show="$store.restore.error" |
|---|
| 558 | | - class="mt-4 card border-red-800 text-red-400 text-sm" |
|---|
| 559 | | - x-text="$store.restore.error"></div> |
|---|
| 560 | | - </div> |
|---|
| 561 | | - |
|---|
| 562 | | - <!-- Confirm dialog --> |
|---|
| 563 | | - <div x-show="$store.restore.confirming" class="modal-overlay" @click.self="$store.restore.cancel()"> |
|---|
| 564 | | - <div class="modal-box max-w-md"> |
|---|
| 565 | | - <div class="modal-header"> |
|---|
| 566 | | - <h3 class="font-semibold text-white">Confirm Restore</h3> |
|---|
| 567 | | - </div> |
|---|
| 568 | | - <div class="modal-body text-sm text-gray-300 space-y-3"> |
|---|
| 569 | | - <p>You are about to restore:</p> |
|---|
| 570 | | - <div class="bg-gray-900 rounded-lg p-3 space-y-2 text-xs font-mono"> |
|---|
| 571 | | - <div><span class="text-gray-500">Project:</span> <span class="text-white" x-text="$store.restore.project"></span></div> |
|---|
| 572 | | - <div><span class="text-gray-500">Environment:</span> <span class="text-white" x-text="$store.restore.env"></span></div> |
|---|
| 573 | | - <div><span class="text-gray-500">Source:</span> <span class="text-white" x-text="$store.restore.source"></span></div> |
|---|
| 574 | | - <div><span class="text-gray-500">Dry Run:</span> <span :class="$store.restore.dryRun ? 'text-yellow-400' : 'text-white'" x-text="$store.restore.dryRun ? 'YES — simulate only' : 'NO — live restore'"></span></div> |
|---|
| 575 | | - </div> |
|---|
| 576 | | - <p x-show="!$store.restore.dryRun" class="text-yellow-400 font-medium"> |
|---|
| 577 | | - This will overwrite existing data. This action cannot be undone. |
|---|
| 578 | | - </p> |
|---|
| 579 | | - </div> |
|---|
| 580 | | - <div class="modal-footer"> |
|---|
| 581 | | - <button class="btn btn-ghost" @click="$store.restore.cancel()">Cancel</button> |
|---|
| 582 | | - <button class="btn btn-danger" @click="$store.restore.execute()"> |
|---|
| 583 | | - <span x-text="$store.restore.dryRun ? 'Run Dry Test' : 'Yes, Restore Now'"></span> |
|---|
| 584 | | - </button> |
|---|
| 585 | | - </div> |
|---|
| 586 | | - </div> |
|---|
| 587 | | - </div> |
|---|
| 588 | | - </div> |
|---|
| 589 | | - |
|---|
| 590 | | - <!-- ====================================================== |
|---|
| 591 | | - SERVICES PAGE |
|---|
| 592 | | - ====================================================== --> |
|---|
| 593 | | - <div x-show="$store.app.page === 'services'" class="p-4 md:p-6 page-enter"> |
|---|
| 594 | | - |
|---|
| 595 | | - <!-- Loading --> |
|---|
| 596 | | - <div x-show="$store.services.loading && !$store.services.projects.length" |
|---|
| 597 | | - class="flex items-center justify-center h-48 text-gray-500"> |
|---|
| 598 | | - <div class="spinner spinner-lg"></div> |
|---|
| 599 | | - </div> |
|---|
| 600 | | - |
|---|
| 601 | | - <!-- Error --> |
|---|
| 602 | | - <div x-show="$store.services.error" class="card border-red-800 text-red-400 text-sm mb-4" |
|---|
| 603 | | - x-text="$store.services.error"></div> |
|---|
| 604 | | - |
|---|
| 605 | | - <!-- Projects --> |
|---|
| 606 | | - <template x-for="proj in $store.services.projects" :key="proj.name"> |
|---|
| 607 | | - <div class="mb-8"> |
|---|
| 608 | | - <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-widest mb-3" |
|---|
| 609 | | - x-text="proj.name"></h3> |
|---|
| 610 | | - |
|---|
| 611 | | - <div class="space-y-2"> |
|---|
| 612 | | - <template x-for="svc in proj.services" :key="svc.name || svc.service"> |
|---|
| 613 | | - <div class="card flex flex-col sm:flex-row sm:items-center gap-3"> |
|---|
| 614 | | - <!-- Status + name --> |
|---|
| 615 | | - <div class="flex items-center gap-3 flex-1 min-w-0"> |
|---|
| 616 | | - <div class="badge flex-shrink-0" |
|---|
| 617 | | - :class="$store.services.badgeClass(svc.status, svc.health)"> |
|---|
| 618 | | - <span class="status-dot" |
|---|
| 619 | | - :class="$store.services.dotClass(svc.status, svc.health)"></span> |
|---|
| 620 | | - <span x-text="svc.status || 'unknown'"></span> |
|---|
| 621 | | - </div> |
|---|
| 622 | | - <div> |
|---|
| 623 | | - <div class="font-medium text-white text-sm" x-text="svc.name || svc.service"></div> |
|---|
| 624 | | - <div class="text-xs text-gray-500" x-text="svc.uptime || ''"></div> |
|---|
| 625 | | - </div> |
|---|
| 626 | | - </div> |
|---|
| 627 | | - |
|---|
| 628 | | - <!-- Actions --> |
|---|
| 629 | | - <div class="flex items-center gap-2 flex-shrink-0"> |
|---|
| 630 | | - <template x-if="svc.domain"> |
|---|
| 631 | | - <a :href="'https://' + svc.domain" target="_blank" rel="noopener" |
|---|
| 632 | | - class="btn btn-ghost btn-xs"> |
|---|
| 633 | | - <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
|---|
| 634 | | - <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/> |
|---|
| 635 | | - </svg> |
|---|
| 636 | | - Open |
|---|
| 637 | | - </a> |
|---|
| 638 | | - </template> |
|---|
| 639 | | - |
|---|
| 640 | | - <button class="btn btn-ghost btn-xs" |
|---|
| 641 | | - @click="$store.services.viewLogs(svc.project || proj.name, svc.env || 'prod', svc.name || svc.service)"> |
|---|
| 642 | | - <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
|---|
| 643 | | - <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 9h16.5m-16.5 6.75h16.5"/> |
|---|
| 644 | | - </svg> |
|---|
| 645 | | - Logs |
|---|
| 646 | | - </button> |
|---|
| 647 | | - |
|---|
| 648 | | - <button class="btn btn-warning btn-xs" |
|---|
| 649 | | - @click="$store.services.askRestart(svc.project || proj.name, svc.env || 'prod', svc.name || svc.service)"> |
|---|
| 650 | | - <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
|---|
| 651 | | - <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-4.566l3.181 3.182m0-4.991v4.99"/> |
|---|
| 652 | | - </svg> |
|---|
| 653 | | - Restart |
|---|
| 654 | | - </button> |
|---|
| 655 | | - </div> |
|---|
| 656 | | - </div> |
|---|
| 657 | | - </template> |
|---|
| 658 | | - </div> |
|---|
| 659 | | - </div> |
|---|
| 660 | | - </template> |
|---|
| 661 | | - |
|---|
| 662 | | - <!-- Log Modal --> |
|---|
| 663 | | - <div x-show="$store.services.logModal.open" class="modal-overlay" @click.self="$store.services.closeLogs()"> |
|---|
| 664 | | - <div class="modal-box" style="max-width: 780px;"> |
|---|
| 665 | | - <div class="modal-header"> |
|---|
| 666 | | - <h3 class="font-semibold text-white text-sm" x-text="$store.services.logModal.title"></h3> |
|---|
| 667 | | - <button class="text-gray-400 hover:text-white text-lg leading-none" @click="$store.services.closeLogs()">✕</button> |
|---|
| 668 | | - </div> |
|---|
| 669 | | - <div class="modal-body p-0"> |
|---|
| 670 | | - <div x-show="$store.services.logModal.loading" |
|---|
| 671 | | - class="flex items-center justify-center h-32 text-gray-500"> |
|---|
| 672 | | - <div class="spinner spinner-lg"></div> |
|---|
| 673 | | - </div> |
|---|
| 674 | | - <div id="log-output" class="terminal rounded-none rounded-b-xl" |
|---|
| 675 | | - x-show="!$store.services.logModal.loading" |
|---|
| 676 | | - style="height: 480px; border-radius: 0 0 0.875rem 0.875rem; border: none;"> |
|---|
| 677 | | - <template x-for="(line, i) in $store.services.logModal.lines" :key="i"> |
|---|
| 678 | | - <div x-text="typeof line === 'string' ? line : (line.text || JSON.stringify(line))"></div> |
|---|
| 679 | | - </template> |
|---|
| 680 | | - </div> |
|---|
| 681 | | - </div> |
|---|
| 682 | | - </div> |
|---|
| 683 | | - </div> |
|---|
| 684 | | - |
|---|
| 685 | | - <!-- Restart Confirm Modal --> |
|---|
| 686 | | - <div x-show="$store.services.confirmRestart.open" class="modal-overlay" |
|---|
| 687 | | - @click.self="$store.services.cancelRestart()"> |
|---|
| 688 | | - <div class="modal-box max-w-md"> |
|---|
| 689 | | - <div class="modal-header"> |
|---|
| 690 | | - <h3 class="font-semibold text-white">Confirm Restart</h3> |
|---|
| 691 | | - </div> |
|---|
| 692 | | - <div class="modal-body text-sm text-gray-300"> |
|---|
| 693 | | - <p>Restart service <strong class="text-white" x-text="$store.services.confirmRestart.service"></strong>?</p> |
|---|
| 694 | | - <p class="text-gray-500 mt-1 text-xs">This will briefly interrupt the service.</p> |
|---|
| 695 | | - </div> |
|---|
| 696 | | - <div class="modal-footer"> |
|---|
| 697 | | - <button class="btn btn-ghost" @click="$store.services.cancelRestart()">Cancel</button> |
|---|
| 698 | | - <button class="btn btn-warning" @click="$store.services.doRestart()" |
|---|
| 699 | | - :disabled="$store.services.confirmRestart.running"> |
|---|
| 700 | | - <span x-show="$store.services.confirmRestart.running" class="spinner"></span> |
|---|
| 701 | | - <span x-text="$store.services.confirmRestart.running ? 'Restarting…' : 'Restart'"></span> |
|---|
| 702 | | - </button> |
|---|
| 703 | | - </div> |
|---|
| 704 | | - </div> |
|---|
| 705 | | - </div> |
|---|
| 706 | | - </div> |
|---|
| 707 | | - |
|---|
| 708 | | - <!-- ====================================================== |
|---|
| 709 | | - SYSTEM PAGE |
|---|
| 710 | | - ====================================================== --> |
|---|
| 711 | | - <div x-show="$store.app.page === 'system'" class="p-4 md:p-6 page-enter space-y-8"> |
|---|
| 712 | | - |
|---|
| 713 | | - <!-- System Info --> |
|---|
| 714 | | - <section> |
|---|
| 715 | | - <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-widest mb-3">System Info</h3> |
|---|
| 716 | | - <div x-show="$store.system.loading.info" class="flex items-center gap-2 text-gray-500 text-sm"> |
|---|
| 717 | | - <div class="spinner"></div> Loading… |
|---|
| 718 | | - </div> |
|---|
| 719 | | - <div x-show="!$store.system.loading.info" class="grid grid-cols-2 sm:grid-cols-4 gap-3"> |
|---|
| 720 | | - <div class="card text-center"> |
|---|
| 721 | | - <div class="text-xs text-gray-500 mb-1">Uptime</div> |
|---|
| 722 | | - <div class="text-white font-medium text-sm" x-text="$store.system.info.uptime || '—'"></div> |
|---|
| 723 | | - </div> |
|---|
| 724 | | - <div class="card text-center"> |
|---|
| 725 | | - <div class="text-xs text-gray-500 mb-1">Load (1m)</div> |
|---|
| 726 | | - <div class="text-white font-medium text-sm" x-text="$store.system.info.load_1 ?? $store.system.info.load_avg?.[0] ?? '—'"></div> |
|---|
| 727 | | - </div> |
|---|
| 728 | | - <div class="card text-center"> |
|---|
| 729 | | - <div class="text-xs text-gray-500 mb-1">Load (5m)</div> |
|---|
| 730 | | - <div class="text-white font-medium text-sm" x-text="$store.system.info.load_5 ?? $store.system.info.load_avg?.[1] ?? '—'"></div> |
|---|
| 731 | | - </div> |
|---|
| 732 | | - <div class="card text-center"> |
|---|
| 733 | | - <div class="text-xs text-gray-500 mb-1">Hostname</div> |
|---|
| 734 | | - <div class="text-white font-medium text-sm truncate" x-text="$store.system.info.hostname || '—'"></div> |
|---|
| 735 | | - </div> |
|---|
| 736 | | - </div> |
|---|
| 737 | | - </section> |
|---|
| 738 | | - |
|---|
| 739 | | - <!-- Disk Usage --> |
|---|
| 740 | | - <section> |
|---|
| 741 | | - <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-widest mb-3">Disk Usage</h3> |
|---|
| 742 | | - <div x-show="$store.system.loading.disk" class="flex items-center gap-2 text-gray-500 text-sm"> |
|---|
| 743 | | - <div class="spinner"></div> Loading… |
|---|
| 744 | | - </div> |
|---|
| 745 | | - <div x-show="!$store.system.loading.disk && !$store.system.disk.length" |
|---|
| 746 | | - class="text-gray-600 text-sm">No disk data</div> |
|---|
| 747 | | - <div x-show="!$store.system.loading.disk && $store.system.disk.length > 0" class="space-y-3"> |
|---|
| 748 | | - <template x-for="(d, i) in $store.system.disk" :key="i"> |
|---|
| 749 | | - <div class="card"> |
|---|
| 750 | | - <div class="flex items-center justify-between mb-2"> |
|---|
| 751 | | - <div> |
|---|
| 752 | | - <span class="text-white font-medium text-sm" x-text="d.mount || d.filesystem || '?'"></span> |
|---|
| 753 | | - <span class="text-gray-500 text-xs ml-2" x-text="d.filesystem || d.device || ''"></span> |
|---|
| 754 | | - </div> |
|---|
| 755 | | - <span class="text-sm font-semibold" |
|---|
| 756 | | - :class="(d.use_pct || d.pct || 0) >= 90 ? 'text-red-400' : (d.use_pct || d.pct || 0) >= 75 ? 'text-yellow-400' : 'text-green-400'" |
|---|
| 757 | | - x-text="(d.use_pct || d.pct || 0) + '%'"></span> |
|---|
| 758 | | - </div> |
|---|
| 759 | | - <div class="progress-bar-track"> |
|---|
| 760 | | - <div class="progress-bar-fill" |
|---|
| 761 | | - :class="$store.system.diskBarClass(d.use_pct || d.pct || 0)" |
|---|
| 762 | | - :style="'width: ' + Math.min(d.use_pct || d.pct || 0, 100) + '%'"></div> |
|---|
| 763 | | - </div> |
|---|
| 764 | | - <div class="flex justify-between text-xs text-gray-500 mt-1"> |
|---|
| 765 | | - <span x-text="'Used: ' + $store.system.formatBytes(d.used)"></span> |
|---|
| 766 | | - <span x-text="'Total: ' + $store.system.formatBytes(d.total || d.size)"></span> |
|---|
| 767 | | - </div> |
|---|
| 768 | | - </div> |
|---|
| 769 | | - </template> |
|---|
| 770 | | - </div> |
|---|
| 771 | | - </section> |
|---|
| 772 | | - |
|---|
| 773 | | - <!-- Health Checks --> |
|---|
| 774 | | - <section> |
|---|
| 775 | | - <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-widest mb-3">Health Checks</h3> |
|---|
| 776 | | - <div x-show="$store.system.loading.health" class="flex items-center gap-2 text-gray-500 text-sm"> |
|---|
| 777 | | - <div class="spinner"></div> Loading… |
|---|
| 778 | | - </div> |
|---|
| 779 | | - <div x-show="!$store.system.loading.health && !$store.system.health.length" |
|---|
| 780 | | - class="text-gray-600 text-sm">No health data</div> |
|---|
| 781 | | - <div x-show="!$store.system.loading.health && $store.system.health.length > 0" class="space-y-2"> |
|---|
| 782 | | - <template x-for="(h, i) in $store.system.health" :key="i"> |
|---|
| 783 | | - <div class="card flex items-center justify-between gap-3"> |
|---|
| 784 | | - <div class="flex items-center gap-3"> |
|---|
| 785 | | - <div class="w-2 h-2 rounded-full flex-shrink-0" |
|---|
| 786 | | - :class="h.ok || h.status === 'pass' ? 'bg-green-400' : 'bg-red-400'"></div> |
|---|
| 787 | | - <span class="text-sm text-gray-300 font-medium" x-text="h.name || h.check"></span> |
|---|
| 788 | | - </div> |
|---|
| 789 | | - <div class="flex items-center gap-2"> |
|---|
| 790 | | - <span class="text-xs text-gray-500" x-text="h.detail || h.message || ''"></span> |
|---|
| 791 | | - <span class="badge" |
|---|
| 792 | | - :class="h.ok || h.status === 'pass' ? 'badge-green' : 'badge-red'" |
|---|
| 793 | | - x-text="h.ok || h.status === 'pass' ? 'PASS' : 'FAIL'"></span> |
|---|
| 794 | | - </div> |
|---|
| 795 | | - </div> |
|---|
| 796 | | - </template> |
|---|
| 797 | | - </div> |
|---|
| 798 | | - </section> |
|---|
| 799 | | - |
|---|
| 800 | | - <!-- Systemd Timers --> |
|---|
| 801 | | - <section> |
|---|
| 802 | | - <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-widest mb-3">Systemd Timers</h3> |
|---|
| 803 | | - <div x-show="$store.system.loading.timers" class="flex items-center gap-2 text-gray-500 text-sm"> |
|---|
| 804 | | - <div class="spinner"></div> Loading… |
|---|
| 805 | | - </div> |
|---|
| 806 | | - <div x-show="!$store.system.loading.timers && !$store.system.timers.length" |
|---|
| 807 | | - class="text-gray-600 text-sm">No timer data</div> |
|---|
| 808 | | - <div x-show="!$store.system.loading.timers && $store.system.timers.length > 0" class="table-wrapper"> |
|---|
| 809 | | - <table class="ops-table"> |
|---|
| 810 | | - <thead> |
|---|
| 811 | | - <tr> |
|---|
| 812 | | - <th>Timer</th> |
|---|
| 813 | | - <th>Last Run</th> |
|---|
| 814 | | - <th>Next Run</th> |
|---|
| 815 | | - <th>Status</th> |
|---|
| 816 | | - </tr> |
|---|
| 817 | | - </thead> |
|---|
| 818 | | - <tbody> |
|---|
| 819 | | - <template x-for="(t, i) in $store.system.timers" :key="i"> |
|---|
| 820 | | - <tr> |
|---|
| 821 | | - <td class="font-medium text-white" x-text="t.name || t.timer"></td> |
|---|
| 822 | | - <td class="text-gray-400 text-xs mono" x-text="t.last || t.last_trigger || '—'"></td> |
|---|
| 823 | | - <td class="text-gray-400 text-xs mono" x-text="t.next || t.next_elapse || '—'"></td> |
|---|
| 824 | | - <td> |
|---|
| 825 | | - <span class="badge" |
|---|
| 826 | | - :class="t.active || t.status === 'active' ? 'badge-green' : 'badge-gray'" |
|---|
| 827 | | - x-text="t.status || (t.active ? 'active' : 'inactive')"></span> |
|---|
| 828 | | - </td> |
|---|
| 829 | | - </tr> |
|---|
| 830 | | - </template> |
|---|
| 831 | | - </tbody> |
|---|
| 832 | | - </table> |
|---|
| 833 | | - </div> |
|---|
| 834 | | - </section> |
|---|
| 835 | | - |
|---|
| 836 | | - </div> |
|---|
| 837 | | - |
|---|
| 838 | | - </main> |
|---|
| 104 | + <!-- Page content --> |
|---|
| 105 | + <div id="page-content"></div> |
|---|
| 839 | 106 | </div> |
|---|
| 840 | 107 | </div> |
|---|
| 841 | 108 | |
|---|
| 109 | +<!-- Toast Container --> |
|---|
| 110 | +<div id="toast-container" class="toast-container"></div> |
|---|
| 111 | + |
|---|
| 112 | +<!-- Log Modal --> |
|---|
| 113 | +<div id="log-modal" class="modal-overlay" style="display:none;" onclick="if(event.target===this)closeLogModal()"> |
|---|
| 114 | + <div class="modal-box" style="max-width:800px;"> |
|---|
| 115 | + <div class="modal-header"> |
|---|
| 116 | + <span id="log-modal-title" style="font-weight:600;color:#f3f4f6;">Container Logs</span> |
|---|
| 117 | + <button onclick="closeLogModal()" style="background:none;border:none;color:#9ca3af;font-size:1.25rem;cursor:pointer;">×</button> |
|---|
| 118 | + </div> |
|---|
| 119 | + <div class="modal-body" style="padding:0;"> |
|---|
| 120 | + <div id="log-modal-content" class="terminal" style="max-height:60vh;border:none;border-radius:0;"></div> |
|---|
| 121 | + </div> |
|---|
| 122 | + <div class="modal-footer"> |
|---|
| 123 | + <button class="btn btn-ghost btn-sm" onclick="closeLogModal()">Close</button> |
|---|
| 124 | + <button class="btn btn-primary btn-sm" id="log-refresh-btn" onclick="refreshLogs()">Refresh</button> |
|---|
| 125 | + </div> |
|---|
| 126 | + </div> |
|---|
| 127 | +</div> |
|---|
| 128 | + |
|---|
| 129 | +<script src="/static/js/app.js"></script> |
|---|
| 842 | 130 | </body> |
|---|
| 843 | 131 | </html> |
|---|