diff --git a/platform/restore-myapps.sh b/platform/restore-myapps.sh index 1f9ab3e..f4b5a89 100755 --- a/platform/restore-myapps.sh +++ b/platform/restore-myapps.sh @@ -158,6 +158,55 @@ container_is_healthy() { return 1 } +# -------------------------------------------------- +# NEW: Detect a crash-looping Postgres container and +# attempt automatic recovery via pg_resetwal if the +# logs show WAL/checkpoint corruption. This is a safety +# net โ the real fix is that backups now stop DB +# containers before tar'ing their volumes โ but this +# protects against older/already-corrupt backups too. +# -------------------------------------------------- +repair_postgres_if_crashlooping() { + local ctr="$1" + local vol="$2" + + [ -z "$ctr" ] || [ -z "$vol" ] && return 0 + docker ps -a --format '{{.Names}}' | grep -qx "$ctr" || return 0 + + sleep 8 # give it a moment to attempt startup + + local restart_count status + restart_count=$(docker inspect --format='{{.RestartCount}}' "$ctr" 2>/dev/null || echo 0) + status=$(docker inspect --format='{{.State.Status}}' "$ctr" 2>/dev/null || echo "missing") + + if [ "$status" = "restarting" ] || [ "$restart_count" -gt 2 ]; then + echo " โ ๏ธ $ctr is crash-looping (status=$status, restarts=$restart_count)" + + local last_log + last_log=$(docker logs "$ctr" --tail 30 2>&1) + + if echo "$last_log" | grep -qi "could not locate a valid checkpoint record\|invalid primary checkpoint\|unexpected pageaddr"; then + echo " ๐ง Detected WAL corruption โ attempting pg_resetwal recovery on volume '$vol'..." + docker stop "$ctr" &>/dev/null || true + docker run --rm -v "${vol}:/var/lib/postgresql/data" postgres:15 \ + bash -c "chown -R postgres:postgres /var/lib/postgresql/data && \ + su postgres -c 'pg_resetwal -f /var/lib/postgresql/data'" \ + && echo " โ pg_resetwal completed" \ + || echo " โ pg_resetwal failed โ manual intervention needed" + docker start "$ctr" &>/dev/null || true + sleep 8 + status=$(docker inspect --format='{{.State.Status}}' "$ctr" 2>/dev/null || echo "missing") + if [ "$status" = "running" ]; then + echo " โ $ctr recovered and running (note: pg_resetwal can drop the most recent transactions โ verify your data)" + else + echo " โ $ctr still not healthy after pg_resetwal โ check 'docker logs $ctr' manually. DATA MAY BE LOST." + fi + else + echo " โ $ctr crash-looping for a reason other than WAL corruption โ check 'docker logs $ctr' manually." + fi + fi +} + # -------------------------------------------------- # STEP 1: Restore Volumes # -------------------------------------------------- @@ -259,6 +308,14 @@ declare -A APP_KEY=( ["n8n"]="n8n" ) +# Postgres-backed apps: map app key -> "container:volume" for the +# crash-loop auto-repair check run right after each app starts. +declare -A APP_POSTGRES=( + ["n8n"]="n8n-postgres:n8n-setup_n8n-db-data" + ["nextcloud"]="nextcloud-postgres:nextcloud-setup_nextcloud-db-data" + ["odoo"]="odoo-clean-db-1:odoo-clean_db-data" +) + if [ -d "$SCRIPT_DIR/compose-files" ]; then cd "$SCRIPT_DIR/compose-files" for app in Frappe Odoo Nextcloud Mautic n8n; do @@ -292,6 +349,14 @@ if [ -d "$SCRIPT_DIR/compose-files" ]; then fi docker-compose up -d 2>&1 | tail -3 cd .. + + # If this app has a Postgres DB, check it didn't come up crash-looping. + pg_spec="${APP_POSTGRES[$app_key]:-}" + if [ -n "$pg_spec" ]; then + pg_ctr="${pg_spec%%:*}" + pg_vol="${pg_spec##*:}" + repair_postgres_if_crashlooping "$pg_ctr" "$pg_vol" + fi done cd "$SCRIPT_DIR" else @@ -475,7 +540,15 @@ else docker start n8n-app 2>/dev/null && echo " โ Started n8n-app" || echo " โ ๏ธ Could not start n8n" fi fi - echo " โ n8n โ http://${VM_IP}:5678" + + # Re-check postgres after this section's potential restart too. + repair_postgres_if_crashlooping "n8n-postgres" "n8n-setup_n8n-db-data" + + if container_is_healthy n8n-postgres; then + echo " โ n8n โ http://${VM_IP}:5678" + else + echo " โ ๏ธ n8n-app is up but n8n-postgres is NOT healthy โ check: docker logs n8n-postgres" + fi fi # -------------------------------------------------- @@ -485,9 +558,19 @@ echo "" echo "=========================================" echo "โ RESTORE COMPLETE" echo "=========================================" -if app_selected nextcloud; then echo " Nextcloud โ http://${VM_IP}:8082"; fi -if app_selected mautic; then echo " Mautic โ http://${VM_IP}:8081/s/login (admin / Admin!Password123)"; fi -if app_selected odoo; then echo " Odoo โ https://odooo.nav.ovh/web"; fi -if app_selected n8n; then echo " n8n โ http://${VM_IP}:5678"; fi -if app_selected frappe; then echo " Frappe โ http://${VM_IP}:8080"; fi + +summarize_app() { + local label="$1" url="$2" db_ctr="$3" + if [ -n "$db_ctr" ] && ! container_is_healthy "$db_ctr"; then + echo " ${label} โ โ ๏ธ DB NOT healthy ($db_ctr) โ check: docker logs $db_ctr" + else + echo " ${label} โ ${url}" + fi +} + +if app_selected nextcloud; then summarize_app "Nextcloud" "http://${VM_IP}:8082" "nextcloud-postgres"; fi +if app_selected mautic; then summarize_app "Mautic " "http://${VM_IP}:8081/s/login (admin / Admin!Password123)" "mautic-mariadb"; fi +if app_selected odoo; then summarize_app "Odoo " "https://odooo.nav.ovh/web" "odoo-clean-db-1"; fi +if app_selected n8n; then summarize_app "n8n " "http://${VM_IP}:5678" "n8n-postgres"; fi +if app_selected frappe; then summarize_app "Frappe " "http://${VM_IP}:8080" "frappe-mariadb"; fi echo "=========================================" \ No newline at end of file diff --git a/platform/static/css/style.css b/platform/static/css/style.css index 6021230..3957295 100644 --- a/platform/static/css/style.css +++ b/platform/static/css/style.css @@ -9,52 +9,55 @@ /* โโ DARK THEME (default) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ :root { - --bg: #0a0b0e; - --surface: #111318; - --surface2: #181c24; - --border: #1e2330; - --border2: #262d3d; + --bg: #07080c; + --surface: #0e1017; + --surface2: #141820; + --border: #1a2030; + --border2: #252d42; - --text: #e8ecf4; - --text2: #8892a4; - --text3: #4a5568; + --text: #eef1f8; + --text2: #8b95aa; + --text3: #525d72; - --accent: #3b82f6; - --accent2: #60a5fa; - --green: #22c55e; - --red: #ef4444; - --yellow: #f59e0b; - --purple: #a78bfa; - --cyan: #22d3ee; + --accent: #4f8cff; + --accent2: #7eb3ff; + --green: #2dd47a; + --red: #f05252; + --yellow: #f5b942; + --purple: #b794ff; + --cyan: #3ee0f0; - --shadow: 0 1px 3px rgba(0,0,0,0.4), 0 4px 16px rgba(0,0,0,0.2); + --shadow: 0 1px 2px rgba(0,0,0,0.35), 0 8px 24px rgba(0,0,0,0.22); + --shadow-md: 0 4px 16px rgba(0,0,0,0.28), 0 12px 40px rgba(0,0,0,0.18); + --shadow-lg: 0 8px 32px rgba(0,0,0,0.4), 0 24px 64px rgba(0,0,0,0.25); + --glow-accent: 0 0 24px rgba(79,140,255,0.18); - --radius: 10px; - --radius-lg: 16px; + --radius: 12px; + --radius-lg: 18px; + --radius-xl: 22px; --font: 'Syne', sans-serif; --mono: 'Geist Mono', monospace; - --trans: 0.18s ease; + --trans: 0.22s cubic-bezier(0.4, 0, 0.2, 1); - /* keep legacy var names used in 173 templates */ --bg2: var(--surface); --bg3: var(--surface2); - --bg4: #1e2535; + --bg4: #1a2235; --purple-d: #6366f1; - --sidebar-w: 220px; + --sidebar-w: 236px; --sans: 'Syne', sans-serif; } /* โโ LIGHT THEME โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ [data-theme="light"] { - --bg: #f0f2f7; + --bg: #eef1f8; --surface: #ffffff; - --surface2: #f5f7fb; - --border: #dde1ed; - --border2: #c8cfe0; + --surface2: #f4f6fb; + --border: #dde3ef; + --border2: #c5cfe0; - --text: #0f1117; + --text: #0c0e14; --text2: #4a5578; - --text3: #9aa3b8; + --text3: #8b95aa; --accent: #2563eb; --accent2: #3b82f6; @@ -64,11 +67,14 @@ --purple: #7c3aed; --cyan: #0891b2; - --shadow: 0 1px 3px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.06); + --shadow: 0 1px 3px rgba(15,23,42,0.06), 0 6px 20px rgba(15,23,42,0.05); + --shadow-md: 0 4px 16px rgba(15,23,42,0.08), 0 12px 32px rgba(15,23,42,0.06); + --shadow-lg: 0 8px 32px rgba(15,23,42,0.1), 0 20px 48px rgba(15,23,42,0.08); + --glow-accent: 0 0 20px rgba(37,99,235,0.12); --bg2: var(--surface); --bg3: var(--surface2); - --bg4: #edf2fb; + --bg4: #e8edf8; } /* โโ RESET โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ @@ -82,29 +88,53 @@ html, body { font-size: 14px; line-height: 1.6; -webkit-font-smoothing: antialiased; - transition: background 0.3s, color 0.3s; + -moz-osx-font-smoothing: grayscale; + transition: background 0.35s, color 0.35s; } -::-webkit-scrollbar { width: 4px; height: 4px; } +body::before { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + background: + radial-gradient(ellipse 90% 60% at 10% -10%, rgba(79,140,255,0.07), transparent 55%), + radial-gradient(ellipse 70% 50% at 95% 20%, rgba(183,148,255,0.05), transparent 50%), + radial-gradient(ellipse 50% 40% at 50% 100%, rgba(62,224,240,0.04), transparent 45%); +} +[data-theme="light"] body::before { + background: + radial-gradient(ellipse 90% 60% at 10% -10%, rgba(37,99,235,0.06), transparent 55%), + radial-gradient(ellipse 70% 50% at 95% 20%, rgba(124,58,237,0.04), transparent 50%); +} + +::-webkit-scrollbar { width: 5px; height: 5px; } ::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; } +::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 99px; } +::-webkit-scrollbar-thumb:hover { background: var(--text3); } /* โโ LAYOUT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ -.layout { display: flex; min-height: 100vh; } +.layout { display: flex; min-height: 100vh; position: relative; z-index: 1; } /* โโ SIDEBAR โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ .sidebar { width: var(--sidebar-w); min-width: var(--sidebar-w); - background: var(--surface); + background: rgba(14,16,23,0.88); + backdrop-filter: blur(20px) saturate(1.4); + -webkit-backdrop-filter: blur(20px) saturate(1.4); border-right: 1px solid var(--border); display: flex; flex-direction: column; position: fixed; top: 0; left: 0; bottom: 0; z-index: 100; - padding: 20px 12px; - transition: background 0.3s, border-color 0.3s; + padding: 22px 14px; + transition: background 0.35s, border-color 0.35s; +} +[data-theme="light"] .sidebar { + background: rgba(255,255,255,0.92); } .sidebar-brand, @@ -120,15 +150,16 @@ a.sidebar-brand-link { } .brand-mark { - width: 36px; height: 36px; - background: var(--accent); - border-radius: 8px; + width: 38px; height: 38px; + background: linear-gradient(135deg, #4f8cff 0%, #6366f1 100%); + border-radius: 11px; display: flex; align-items: center; justify-content: center; - font-family: var(--mono); font-weight: 500; font-size: 12px; color: #fff; + font-family: var(--mono); font-weight: 600; font-size: 12px; color: #fff; flex-shrink: 0; + box-shadow: 0 4px 14px rgba(79,140,255,0.35); } -.brand-name { font-size: 15px; font-weight: 700; letter-spacing: 0.02em; } -.brand-sub { font-size: 10px; color: var(--text3); font-family: var(--mono); letter-spacing: 0.08em; } +.brand-name { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; } +.brand-sub { font-size: 9px; color: var(--text3); font-family: var(--mono); letter-spacing: 0.14em; margin-top: 1px; } /* nav */ .nav { flex: 1; display: flex; flex-direction: column; gap: 2px; overflow-y: auto; } @@ -140,23 +171,38 @@ a.sidebar-brand-link { } .nav-item { display: flex; align-items: center; gap: 10px; - padding: 9px 12px; border-radius: var(--radius); + padding: 10px 12px; border-radius: var(--radius); color: var(--text2); text-decoration: none; font-size: 13px; font-weight: 500; cursor: pointer; - transition: background var(--trans), color var(--trans); - margin-bottom: 1px; + transition: background var(--trans), color var(--trans), transform var(--trans), box-shadow var(--trans); + margin-bottom: 2px; + border: 1px solid transparent; + position: relative; } -.nav-item i { width: 16px; text-align: center; font-size: 13px; } +.nav-item i { width: 16px; text-align: center; font-size: 13px; opacity: 0.85; } .nav-item span { flex: 1; } -.nav-item:hover { background: var(--surface2); color: var(--text); } -.nav-item.active { background: var(--accent); color: #fff; } +.nav-item:hover { background: var(--surface2); color: var(--text); transform: translateX(2px); } +.nav-item.active { + background: linear-gradient(90deg, rgba(79,140,255,0.16) 0%, rgba(79,140,255,0.04) 100%); + color: var(--text); + border-color: rgba(79,140,255,0.22); + box-shadow: inset 3px 0 0 var(--accent); +} +.nav-item.active i { color: var(--accent2); opacity: 1; } +[data-theme="light"] .nav-item.active { + background: linear-gradient(90deg, rgba(37,99,235,0.1) 0%, rgba(37,99,235,0.02) 100%); +} .nav-badge { margin-left: auto; font-family: var(--mono); font-size: 10px; - background: var(--border2); color: var(--text2); - padding: 2px 6px; border-radius: 20px; + background: var(--bg4); color: var(--text2); + padding: 2px 7px; border-radius: 20px; border: 1px solid var(--border); + font-weight: 600; +} +.nav-item.active .nav-badge { + background: rgba(79,140,255,0.2); color: var(--accent2); + border-color: rgba(79,140,255,0.25); } -.nav-item.active .nav-badge { background: rgba(255,255,255,0.25); color: #fff; border-color: transparent; } /* sidebar footer */ .sidebar-footer { @@ -167,8 +213,9 @@ a.sidebar-brand-link { .status-pill { flex: 1; display: flex; align-items: center; gap: 7px; background: var(--surface2); border-radius: 20px; - padding: 6px 10px; font-family: var(--mono); + padding: 7px 12px; font-family: var(--mono); font-size: 11px; color: var(--text2); overflow: hidden; + border: 1px solid var(--border); } .pulse-dot { width: 7px; height: 7px; border-radius: 50%; @@ -194,14 +241,18 @@ a.sidebar-brand-link { /* theme toggle */ .theme-toggle { - width: 32px; height: 32px; + width: 34px; height: 34px; display: flex; align-items: center; justify-content: center; - border-radius: 8px; background: var(--surface2); + border-radius: 10px; background: var(--surface2); border: 1px solid var(--border); color: var(--text2); cursor: pointer; font-size: 14px; - transition: background var(--trans), color var(--trans), border-color var(--trans); + transition: background var(--trans), color var(--trans), border-color var(--trans), box-shadow var(--trans); +} +.theme-toggle:hover { + background: var(--bg4); color: var(--accent2); + border-color: rgba(79,140,255,0.3); + box-shadow: var(--glow-accent); } -.theme-toggle:hover { background: var(--border2); color: var(--text); } /* โโ MAIN โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ .main { @@ -214,109 +265,203 @@ a.sidebar-brand-link { /* โโ TOPBAR โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ .topbar { display: flex; align-items: center; justify-content: space-between; - padding: 18px 28px; border-bottom: 1px solid var(--border); - background: var(--surface); flex-shrink: 0; + padding: 20px 32px; + border-bottom: 1px solid var(--border); + background: rgba(14,16,23,0.75); + backdrop-filter: blur(16px) saturate(1.3); + -webkit-backdrop-filter: blur(16px) saturate(1.3); + flex-shrink: 0; position: sticky; top: 0; z-index: 50; - transition: background 0.3s, border-color 0.3s; + transition: background 0.35s, border-color 0.35s; } -.topbar-left { display: flex; align-items: baseline; gap: 12px; } -.page-title { font-size: 20px; font-weight: 700; } +[data-theme="light"] .topbar { background: rgba(255,255,255,0.82); } +.topbar-left { display: flex; align-items: baseline; gap: 14px; } +.page-title { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; } .page-subtitle, -.page-sub { font-family: var(--mono); font-size: 11px; color: var(--text3); } +.page-sub { font-family: var(--mono); font-size: 11px; color: var(--text3); padding: 3px 10px; background: var(--surface2); border-radius: 20px; border: 1px solid var(--border); } .topbar-right { display: flex; align-items: center; gap: 10px; } .icon-btn { - width: 34px; height: 34px; background: var(--surface2); - border: 1px solid var(--border); border-radius: 8px; + width: 36px; height: 36px; background: var(--surface2); + border: 1px solid var(--border); border-radius: 10px; color: var(--text2); cursor: pointer; display: flex; align-items: center; justify-content: center; - transition: background var(--trans), color var(--trans); font-size: 13px; + transition: background var(--trans), color var(--trans), border-color var(--trans), box-shadow var(--trans); + font-size: 13px; } -.icon-btn:hover { background: var(--border2); color: var(--text); } +.icon-btn:hover { background: var(--bg4); color: var(--accent2); border-color: rgba(79,140,255,0.3); box-shadow: var(--glow-accent); } .icon-btn.spinning i, .icon-btn .spinning { animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .uptime-chip { - font-family: var(--mono); font-size: 11px; color: var(--text3); + font-family: var(--mono); font-size: 11px; color: var(--text2); background: var(--surface2); border: 1px solid var(--border); - padding: 5px 12px; border-radius: 20px; + padding: 6px 14px; border-radius: 20px; + display: flex; align-items: center; gap: 6px; +} +.uptime-chip::before { + content: ''; width: 6px; height: 6px; border-radius: 50%; + background: var(--green); box-shadow: 0 0 8px rgba(45,212,122,0.5); } /* โโ CONTENT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ -.content { flex: 1; padding: 24px 28px; display: flex; flex-direction: column; gap: 20px; } +.content { + flex: 1; padding: 28px 32px; + display: flex; flex-direction: column; gap: 22px; +} +.content > .card, +.content > .metrics-row, +.content > .dash-charts, +.content > .two-col { animation: contentIn 0.45s cubic-bezier(0.22, 1, 0.36, 1) backwards; } +.content > *:nth-child(1) { animation-delay: 0.04s; } +.content > *:nth-child(2) { animation-delay: 0.08s; } +.content > *:nth-child(3) { animation-delay: 0.12s; } +.content > *:nth-child(4) { animation-delay: 0.16s; } +.content > *:nth-child(5) { animation-delay: 0.20s; } +@keyframes contentIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} /* โโ CARDS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ .card { - background: var(--surface); border: 1px solid var(--border); - border-radius: var(--radius-lg); padding: 20px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 22px 24px; box-shadow: var(--shadow); - transition: background 0.3s, border-color 0.3s; - margin-bottom: 0; + transition: background 0.35s, border-color 0.35s, box-shadow var(--trans); + position: relative; + overflow: hidden; } +.card::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(79,140,255,0.25), rgba(183,148,255,0.15), transparent); + pointer-events: none; +} +.card:hover { box-shadow: var(--shadow-md); border-color: var(--border2); } .card-header { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 16px; + margin-bottom: 18px; } .card-title { - font-size: 13px; font-weight: 600; letter-spacing: 0.04em; - display: flex; align-items: center; gap: 8px; color: var(--text); + font-size: 13px; font-weight: 700; letter-spacing: 0.06em; + text-transform: uppercase; + display: flex; align-items: center; gap: 9px; color: var(--text); +} +.card-title i { + color: var(--accent2); font-size: 12px; + width: 28px; height: 28px; + display: flex; align-items: center; justify-content: center; + background: rgba(79,140,255,0.1); + border-radius: 8px; + border: 1px solid rgba(79,140,255,0.15); } -.card-title i { color: var(--accent2); font-size: 12px; } .card-meta, .badge-chip { font-family: var(--mono); font-size: 11px; color: var(--text3); background: var(--surface2); border: 1px solid var(--border); - padding: 3px 8px; border-radius: 20px; + padding: 4px 10px; border-radius: 20px; } /* โโ METRICS ROW โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ .metrics-row { display: grid; grid-template-columns: repeat(4, 1fr); - gap: 14px; + gap: 16px; } .metric-card { - background: var(--surface2); border: 1px solid var(--border); - border-radius: var(--radius); padding: 16px 18px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 20px 22px; position: relative; overflow: hidden; - transition: background 0.3s, border-color 0.3s; + transition: transform var(--trans), box-shadow var(--trans), border-color var(--trans); + box-shadow: var(--shadow); +} +.metric-card:hover { + transform: translateY(-3px); + box-shadow: var(--shadow-md); + border-color: var(--border2); } .metric-card::before { - content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; - background: var(--accent); + content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; + background: linear-gradient(90deg, var(--accent), var(--accent2)); + border-radius: var(--radius-lg) var(--radius-lg) 0 0; } -.metric-card:nth-child(2)::before { background: var(--purple); } -.metric-card:nth-child(3)::before { background: var(--cyan); } -.metric-card:nth-child(4)::before { background: var(--yellow); } +.metric-card:nth-child(2)::before { background: linear-gradient(90deg, var(--purple), #c4b5fd); } +.metric-card:nth-child(3)::before { background: linear-gradient(90deg, var(--cyan), #67e8f9); } +.metric-card:nth-child(4)::before { background: linear-gradient(90deg, var(--yellow), #fcd34d); } +.metric-card::after { + content: ''; + position: absolute; + top: -30px; right: -30px; + width: 100px; height: 100px; + border-radius: 50%; + pointer-events: none; + opacity: 0.5; +} +.metric-card.cpu::after { background: radial-gradient(circle, rgba(79,140,255,0.12), transparent 70%); } +.metric-card.mem::after { background: radial-gradient(circle, rgba(183,148,255,0.12), transparent 70%); } +.metric-card.disk::after { background: radial-gradient(circle, rgba(62,224,240,0.12), transparent 70%); } +.metric-card.load::after { background: radial-gradient(circle, rgba(245,185,66,0.12), transparent 70%); } + +.metric-top { + display: flex; align-items: flex-start; justify-content: space-between; + margin-bottom: 10px; +} +.metric-icon { + width: 36px; height: 36px; border-radius: 10px; + display: flex; align-items: center; justify-content: center; + font-size: 14px; flex-shrink: 0; + background: rgba(79,140,255,0.1); + border: 1px solid rgba(79,140,255,0.15); + color: var(--accent2); +} +.metric-card.mem .metric-icon { background: rgba(183,148,255,0.1); border-color: rgba(183,148,255,0.15); color: var(--purple); } +.metric-card.disk .metric-icon { background: rgba(62,224,240,0.1); border-color: rgba(62,224,240,0.15); color: var(--cyan); } +.metric-card.load .metric-icon { background: rgba(245,185,66,0.1); border-color: rgba(245,185,66,0.15); color: var(--yellow); } .metric-label { font-family: var(--mono); font-size: 9px; font-weight: 700; - letter-spacing: 0.12em; color: var(--text3); margin-bottom: 8px; + letter-spacing: 0.14em; color: var(--text3); } .metric-value { - font-family: var(--mono); font-size: 26px; font-weight: 700; - letter-spacing: -0.02em; color: var(--text); - line-height: 1; margin-bottom: 10px; + font-family: var(--mono); font-size: 28px; font-weight: 700; + letter-spacing: -0.03em; color: var(--text); + line-height: 1; margin-bottom: 14px; } -.metric-value.small { font-size: 16px; } +.metric-value span { font-size: 14px; color: var(--text3); font-weight: 500; margin-left: 2px; } +.metric-value.small { font-size: 17px; line-height: 1.35; margin-bottom: 14px; } .metric-unit { font-size: 13px; color: var(--text3); font-weight: 400; } -/* unified gauge names (83 uses gauge-bar/gauge-fill; 173 uses gauge-track/gauge-fill) */ .gauge-bar, -.gauge-track { height: 3px; background: var(--border2); border-radius: 4px; overflow: hidden; } +.gauge-track { height: 5px; background: var(--border2); border-radius: 99px; overflow: hidden; } .gauge-fill { - height: 100%; border-radius: 4px; - background: var(--accent); - transition: width 0.6s ease; max-width: 100%; + height: 100%; border-radius: 99px; + background: linear-gradient(90deg, var(--accent), var(--accent2)); + transition: width 0.8s cubic-bezier(0.34, 1.2, 0.64, 1); + max-width: 100%; + box-shadow: 0 0 10px rgba(79,140,255,0.35); } -.metric-card:nth-child(2) .gauge-fill { background: var(--purple); } -.metric-card:nth-child(3) .gauge-fill { background: var(--cyan); } -.metric-card:nth-child(4) .gauge-fill { background: var(--yellow); } +.metric-card:nth-child(2) .gauge-fill { background: linear-gradient(90deg, var(--purple), #c4b5fd); box-shadow: 0 0 10px rgba(183,148,255,0.3); } +.metric-card:nth-child(3) .gauge-fill { background: linear-gradient(90deg, var(--cyan), #67e8f9); box-shadow: 0 0 10px rgba(62,224,240,0.3); } +.metric-card:nth-child(4) .gauge-fill { background: linear-gradient(90deg, var(--yellow), #fcd34d); box-shadow: 0 0 10px rgba(245,185,66,0.3); } + +@media (max-width: 1100px) { .metrics-row { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 560px) { .metrics-row { grid-template-columns: 1fr; } } /* โโ STAT ROW โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ /* 173 uses stat-box; 83 uses stat-card โ support both */ -.stat-row { display: flex; gap: 0; } +.stat-row { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); + gap: 10px; +} .stat-box { flex: 1; text-align: center; @@ -335,12 +480,39 @@ a.sidebar-brand-link { .stat-lbl { font-size: 10px; color: var(--text3); letter-spacing: 0.06em; } .stat-row .stat-card { - flex: 1; text-align: center; - background: var(--surface2); border: 1px solid var(--border); - border-radius: var(--radius); padding: 14px 16px; + text-align: center; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px 12px; + transition: transform var(--trans), border-color var(--trans), box-shadow var(--trans), background var(--trans); + position: relative; + overflow: hidden; +} +.stat-row .stat-card::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(160deg, rgba(79,140,255,0.04) 0%, transparent 60%); + pointer-events: none; +} +.stat-row .stat-card:hover { + transform: translateY(-2px); + border-color: rgba(79,140,255,0.25); + box-shadow: var(--glow-accent); + background: var(--bg4); +} +.stat-number { + font-family: var(--mono); font-size: 30px; font-weight: 700; + color: var(--accent2); line-height: 1; + letter-spacing: -0.03em; + position: relative; +} +.stat-label { + font-size: 10px; color: var(--text3); margin-top: 7px; + letter-spacing: 0.08em; text-transform: uppercase; + font-weight: 600; position: relative; } -.stat-number { font-family: var(--mono); font-size: 28px; font-weight: 500; color: var(--accent2); line-height: 1; } -.stat-label { font-size: 11px; color: var(--text3); margin-top: 5px; letter-spacing: 0.04em; } /* โโ TABLES โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ .table-wrap, @@ -352,19 +524,32 @@ a.sidebar-brand-link { .data-table th, .ct-table th { text-align: left; - font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 0.1em; - color: var(--text3); padding: 6px 12px 10px; + font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text3); padding: 8px 14px 12px; border-bottom: 1px solid var(--border); + background: var(--surface2); } .data-table td, .ct-table td { - padding: 10px 12px; border-bottom: 1px solid var(--border); + padding: 11px 14px; border-bottom: 1px solid var(--border); vertical-align: middle; } .data-table tr:last-child td, .ct-table tr:last-child td { border-bottom: none; } +.data-table tbody tr, +.ct-table tbody tr { transition: background var(--trans); } .data-table tr:hover td, -.ct-table tr:hover td { background: var(--surface2); } +.ct-table tr:hover td { background: rgba(79,140,255,0.04); } +[data-theme="light"] .data-table tr:hover td, +[data-theme="light"] .ct-table tr:hover td { background: rgba(37,99,235,0.03); } +.ct-table-wrap, +.table-wrap { + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; + background: var(--surface2); +} .ct-name { font-family: var(--mono); font-weight: 500; font-size: 12px; color: var(--text); } .ct-image { font-family: var(--mono); font-size: 11px; color: var(--text3); max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } @@ -436,20 +621,37 @@ a.sidebar-brand-link { /* โโ BUTTONS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ .btn { display: inline-flex; align-items: center; gap: 7px; - padding: 9px 16px; border-radius: 8px; + padding: 9px 16px; border-radius: 10px; font-size: 13px; font-weight: 600; font-family: var(--font); cursor: pointer; border: none; text-decoration: none; - transition: opacity var(--trans), filter var(--trans), transform 0.1s; + transition: opacity var(--trans), filter var(--trans), transform var(--trans), box-shadow var(--trans); } -.btn:hover { filter: brightness(1.1); transform: translateY(-1px); } +.btn:hover { filter: brightness(1.08); transform: translateY(-1px); } .btn:active { transform: translateY(0); } -.btn:disabled { opacity: 0.4; cursor: not-allowed; filter: none; transform: none; } +.btn:disabled { opacity: 0.4; cursor: not-allowed; filter: none; transform: none; box-shadow: none; } -.btn-primary, .btn.primary { background: var(--accent); color: #fff; } -.btn-danger, .btn.danger { background: rgba(239,68,68,0.15); color: var(--red); border: 1px solid rgba(239,68,68,0.3); } -.btn-success { background: var(--green); color: #fff; } -.btn-ghost, .btn.ghost { background: var(--surface2); border: 1px solid var(--border2); color: var(--text2); } -.btn-ghost:hover, .btn.ghost:hover { background: var(--border2); color: var(--text); filter: none; } +.btn-primary, .btn.primary { + background: linear-gradient(135deg, var(--accent) 0%, #6366f1 100%); + color: #fff; + box-shadow: 0 4px 14px rgba(79,140,255,0.3); +} +.btn-primary:hover, .btn.primary:hover { box-shadow: 0 6px 20px rgba(79,140,255,0.4); } +.btn-danger, .btn.danger { + background: rgba(240,82,82,0.1); color: var(--red); + border: 1px solid rgba(240,82,82,0.28); +} +.btn-danger:hover, .btn.danger:hover { background: rgba(240,82,82,0.16); box-shadow: 0 4px 14px rgba(240,82,82,0.15); } +.btn-success { background: linear-gradient(135deg, var(--green), #16a34a); color: #fff; box-shadow: 0 4px 14px rgba(45,212,122,0.25); } +.btn-ghost, .btn.ghost { + background: var(--surface2); + border: 1px solid var(--border2); + color: var(--text2); +} +.btn-ghost:hover, .btn.ghost:hover { + background: var(--bg4); color: var(--text); + border-color: rgba(79,140,255,0.25); + filter: none; +} .btn-sm, .btn.sm { padding: 5px 11px; font-size: 12px; } .btn-lg, .btn.lg { padding: 12px 24px; font-size: 14px; } @@ -468,11 +670,14 @@ a.sidebar-brand-link { } .form-input { background: var(--surface2); border: 1px solid var(--border2); - border-radius: 8px; padding: 9px 12px; font-size: 13px; + border-radius: 10px; padding: 10px 14px; font-size: 13px; font-family: var(--mono); color: var(--text); width: 100%; - transition: border-color var(--trans), background 0.3s; + transition: border-color var(--trans), background 0.3s, box-shadow var(--trans); +} +.form-input:focus { + outline: none; border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(79,140,255,0.12); } -.form-input:focus { outline: none; border-color: var(--accent); } .form-input[readonly] { opacity: 0.6; cursor: default; } .form-row { display: flex; gap: 12px; } .form-row .form-group { flex: 1; } @@ -487,12 +692,17 @@ a.sidebar-brand-link { .radio-card input { display: none; } .radio-body { display: flex; align-items: center; gap: 10px; - padding: 12px 14px; border: 1px solid var(--border2); + padding: 13px 16px; border: 1px solid var(--border2); border-radius: var(--radius); background: var(--surface2); - transition: border-color var(--trans), background var(--trans); + transition: border-color var(--trans), background var(--trans), box-shadow var(--trans); } +.radio-card:hover .radio-body { border-color: var(--border2); background: var(--bg4); } .radio-card.sm .radio-body { padding: 9px 12px; min-width: 110px; } -.radio-card input:checked + .radio-body { border-color: var(--accent); background: rgba(59,130,246,0.08); } +.radio-card input:checked + .radio-body { + border-color: rgba(79,140,255,0.45); + background: rgba(79,140,255,0.08); + box-shadow: 0 0 0 1px rgba(79,140,255,0.1), var(--glow-accent); +} [data-theme="light"] .radio-card input:checked + .radio-body { background: rgba(37,99,235,0.06); } .radio-icon { font-size: 16px; color: var(--accent2); } .radio-label { font-size: 13px; font-weight: 600; } @@ -549,16 +759,23 @@ a.sidebar-brand-link { gap: 12px; } .user-card { - padding: 16px; border-radius: 12px; - background: var(--surface2); border: 1px solid var(--border); + padding: 18px; border-radius: var(--radius-lg); + background: var(--surface); + border: 1px solid var(--border); cursor: pointer; - transition: border-color 0.15s, background 0.15s, box-shadow 0.15s; + transition: border-color var(--trans), background var(--trans), box-shadow var(--trans), transform var(--trans); + box-shadow: var(--shadow); +} +.user-card:hover { + border-color: rgba(79,140,255,0.35); + background: var(--bg4); + transform: translateY(-2px); + box-shadow: var(--shadow-md); } -.user-card:hover { border-color: var(--accent); background: var(--bg4); } .user-card.selected { - border-color: var(--accent); - background: rgba(59,130,246,0.08); - box-shadow: 0 0 0 1px rgba(59,130,246,0.15); + border-color: rgba(79,140,255,0.45); + background: rgba(79,140,255,0.07); + box-shadow: 0 0 0 1px rgba(79,140,255,0.15), var(--glow-accent); } [data-theme="light"] .user-card.selected { background: rgba(37,99,235,0.06); } .user-card-top { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; } @@ -605,11 +822,14 @@ a.sidebar-brand-link { margin-bottom: 10px; font-size: 12px; font-weight: 600; color: var(--text2); } .log-console { - background: #05060a; border: 1px solid var(--border); - border-radius: var(--radius); padding: 14px 16px; - font-family: var(--mono); font-size: 12px; line-height: 1.7; - max-height: 400px; overflow-y: auto; color: #8892a4; + background: linear-gradient(180deg, #040508 0%, #080a10 100%); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px 18px; + font-family: var(--mono); font-size: 12px; line-height: 1.75; + max-height: 400px; overflow-y: auto; color: #8b95aa; white-space: pre-wrap; word-break: break-all; + box-shadow: inset 0 2px 12px rgba(0,0,0,0.35); } [data-theme="light"] .log-console { background: #1a1f2e; border-color: #2d3555; } .log-line { margin: 0; } @@ -619,9 +839,15 @@ a.sidebar-brand-link { .backup-list { display: flex; flex-direction: column; gap: 8px; } .backup-item { display: flex; align-items: center; justify-content: space-between; - padding: 9px 12px; border-radius: 8px; + padding: 11px 14px; border-radius: var(--radius); background: var(--surface2); gap: 10px; flex-wrap: wrap; border: 1px solid var(--border); + transition: border-color var(--trans), background var(--trans), transform var(--trans); +} +.backup-item:hover { + border-color: rgba(79,140,255,0.25); + background: var(--bg4); + transform: translateX(2px); } .backup-name { font-family: var(--mono); font-size: 12px; color: var(--text2); flex: 1; min-width: 0; word-break: break-all; } .backup-actions { display: flex; gap: 5px; align-items: center; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; } @@ -633,38 +859,49 @@ a.sidebar-brand-link { /* โโ MODAL โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ .modal-overlay { position: fixed; inset: 0; z-index: 1000; - background: rgba(0,0,0,0.65); - backdrop-filter: blur(3px); + background: rgba(4,5,8,0.78); + backdrop-filter: blur(10px) saturate(1.2); + -webkit-backdrop-filter: blur(10px) saturate(1.2); display: flex; align-items: center; justify-content: center; - padding: 20px; animation: fadeIn 0.15s ease; + padding: 20px; animation: fadeIn 0.2s ease; } .modal-box { - background: #111318; border: 1px solid #1e2330; - border-radius: 16px; width: 100%; max-width: 600px; + background: var(--surface); + border: 1px solid var(--border2); + border-radius: var(--radius-xl); + width: 100%; max-width: 600px; max-height: 88vh; overflow-y: auto; padding: 24px; - box-shadow: 0 32px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04); + box-shadow: var(--shadow-lg), 0 0 0 1px rgba(255,255,255,0.04); position: relative; + animation: modalIn 0.28s cubic-bezier(0.34, 1.4, 0.64, 1); } -[data-theme="light"] .modal-box { background: #fff; border-color: #dde1ed; box-shadow: 0 20px 60px rgba(0,0,0,0.15); } +@keyframes modalIn { + from { opacity: 0; transform: translateY(16px) scale(0.97); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +[data-theme="light"] .modal-box { box-shadow: var(--shadow-lg); } .modal-header { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 20px; padding-bottom: 14px; border-bottom: 1px solid #1e2330; + margin-bottom: 20px; padding-bottom: 14px; + border-bottom: 1px solid var(--border); } -[data-theme="light"] .modal-header { border-bottom-color: #dde1ed; } -.modal-title { font-size: 15px; font-weight: 700; display: flex; align-items: center; gap: 8px; color: #e8ecf4; } -[data-theme="light"] .modal-title { color: #0f1117; } +[data-theme="light"] .modal-header { border-bottom-color: var(--border); } +.modal-title { font-size: 15px; font-weight: 700; display: flex; align-items: center; gap: 8px; color: var(--text); } +[data-theme="light"] .modal-title { color: var(--text); } .modal-close { - background: none; border: none; cursor: pointer; color: #4a5568; - font-size: 18px; line-height: 1; padding: 6px 10px; border-radius: 8px; - transition: background 0.15s, color 0.15s; + background: var(--surface2); border: 1px solid var(--border); + cursor: pointer; color: var(--text3); + font-size: 14px; line-height: 1; padding: 6px 10px; border-radius: 8px; + transition: background 0.15s, color 0.15s, border-color 0.15s; + width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; } -.modal-close:hover { background: rgba(239,68,68,0.12); color: #ef4444; } -.modal-body { padding: 20px 24px 24px; } +.modal-close:hover { background: rgba(240,82,82,0.12); color: var(--red); border-color: rgba(240,82,82,0.25); } +.modal-body { padding: 4px 0 0; } .audit-footer { display: flex; gap: 8px; margin-top: 18px; padding-top: 14px; - border-top: 1px solid #1e2330; + border-top: 1px solid var(--border); } -[data-theme="light"] .audit-footer { border-top-color: #dde1ed; } +[data-theme="light"] .audit-footer { border-top-color: var(--border); } /* โโ NOTICE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ .notice { @@ -686,11 +923,15 @@ a.sidebar-brand-link { /* โโ EMPTY STATE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ .empty-state, .empty { - text-align: center; padding: 36px; color: var(--text3); - font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 8px; + text-align: center; padding: 40px 24px; color: var(--text3); + font-size: 13px; + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; + background: var(--surface2); + border: 1px dashed var(--border2); + border-radius: var(--radius); } -.empty-state i { font-size: 26px; display: block; margin-bottom: 10px; opacity: 0.35; } -.empty.warn { color: var(--yellow); } +.empty-state i { font-size: 28px; opacity: 0.3; color: var(--text3); } +.empty.warn { color: var(--yellow); border-color: rgba(245,185,66,0.25); background: rgba(245,185,66,0.04); } /* โโ SETTINGS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ .settings-grid { display: flex; flex-direction: column; gap: 0; } @@ -712,7 +953,10 @@ a.sidebar-brand-link { /* โโ UTILITIES โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ .row-gap { display: flex; align-items: center; gap: 8px; } .section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } -.section-title { font-size: 13px; font-weight: 600; color: var(--text2); font-family: var(--mono); letter-spacing: 0.06em; } +.section-title { + font-size: 11px; font-weight: 700; color: var(--text2); + font-family: var(--mono); letter-spacing: 0.1em; text-transform: uppercase; +} /* โโ ANIMATION โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ @keyframes fadeIn { @@ -721,4 +965,102 @@ a.sidebar-brand-link { } /* manual backup wrapper */ -#manual-backup-wrapper { border-top: 1px solid var(--border); padding-top: 16px; } \ No newline at end of file +#manual-backup-wrapper { border-top: 1px solid var(--border); padding-top: 16px; } + +/* โโ DASHBOARD CHARTS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ +.dash-charts { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 18px; +} +.dash-chart-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 22px; + display: flex; + flex-direction: column; + align-items: center; + min-height: 280px; + box-shadow: var(--shadow); + transition: transform var(--trans), box-shadow var(--trans), border-color var(--trans); + position: relative; + overflow: hidden; +} +.dash-chart-card::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; height: 1px; + background: linear-gradient(90deg, transparent, rgba(79,140,255,0.2), transparent); +} +.dash-chart-card:hover { + transform: translateY(-3px); + box-shadow: var(--shadow-md); + border-color: var(--border2); +} +.dash-chart-card h3 { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text2); + margin: 0 0 16px; + align-self: flex-start; + display: flex; + align-items: center; + gap: 9px; + width: 100%; +} +.dash-chart-card h3 i { + color: var(--accent2); + font-size: 11px; + width: 26px; height: 26px; + display: flex; align-items: center; justify-content: center; + background: rgba(79,140,255,0.1); + border-radius: 7px; + border: 1px solid rgba(79,140,255,0.12); +} +.dash-chart-wrap { + position: relative; + width: 100%; + max-width: 210px; + height: 210px; + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} +.dash-chart-legend { + display: flex; + flex-wrap: wrap; + gap: 8px 14px; + justify-content: center; + margin-top: 14px; + font-family: var(--mono); + font-size: 10px; + color: var(--text2); + width: 100%; +} +.dash-chart-legend span { + display: inline-flex; align-items: center; gap: 6px; + padding: 4px 10px; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 20px; +} +.dash-chart-legend .dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; } +.dash-quick-links { + display: flex; + gap: 8px; + margin-top: 14px; + flex-wrap: wrap; + justify-content: center; + width: 100%; + padding-top: 14px; + border-top: 1px solid var(--border); +} +@media (max-width: 1100px) { + .dash-charts { grid-template-columns: 1fr; } + .content { padding: 20px 18px; } + .topbar { padding: 16px 18px; } +} \ No newline at end of file diff --git a/platform/templates/base.html b/platform/templates/base.html index 9ca5a5c..bb17a1f 100644 --- a/platform/templates/base.html +++ b/platform/templates/base.html @@ -7,7 +7,7 @@ - +