From ae267a6d9b72fc8c2a0acc71f3ecbc0d4594a4a5 Mon Sep 17 00:00:00 2001 From: ameniboukattaya Date: Sun, 21 Jun 2026 18:56:18 +0100 Subject: [PATCH] Change : UI/UX design --- platform/restore-myapps.sh | 95 +++- platform/static/css/style.css | 666 ++++++++++++++++++------ platform/templates/base.html | 2 +- platform/templates/login.html | 87 ++-- platform/templates/pages/dashboard.html | 95 +--- scripts/backup-myapps.sh | 96 +++- 6 files changed, 765 insertions(+), 276 deletions(-) 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 @@ - +
diff --git a/platform/templates/login.html b/platform/templates/login.html index ce1e0f6..59e5036 100644 --- a/platform/templates/login.html +++ b/platform/templates/login.html @@ -1,5 +1,5 @@ - + @@ -10,53 +10,82 @@ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Syne', sans-serif; - background: #0a0b0e; + background: #07080c; display: flex; align-items: center; justify-content: center; - min-height: 100vh; color: #e8ecf4; + min-height: 100vh; color: #eef1f8; + -webkit-font-smoothing: antialiased; } body::before { - content: ''; position: fixed; inset: 0; + content: ''; position: fixed; inset: 0; pointer-events: none; + background: + radial-gradient(ellipse 80% 60% at 50% -10%, rgba(79,140,255,0.12), transparent 55%), + radial-gradient(ellipse 50% 40% at 100% 80%, rgba(183,148,255,0.08), transparent 50%); + } + body::after { + content: ''; position: fixed; inset: 0; pointer-events: none; background-image: - linear-gradient(rgba(59,130,246,0.03) 1px, transparent 1px), - linear-gradient(90deg, rgba(59,130,246,0.03) 1px, transparent 1px); - background-size: 40px 40px; pointer-events: none; + linear-gradient(rgba(79,140,255,0.025) 1px, transparent 1px), + linear-gradient(90deg, rgba(79,140,255,0.025) 1px, transparent 1px); + background-size: 48px 48px; } .card { - background: #111318; border: 1px solid #1e2330; - border-radius: 20px; padding: 44px 40px; width: 380px; position: relative; + background: rgba(14,16,23,0.9); + backdrop-filter: blur(24px); + border: 1px solid #1a2030; + border-radius: 22px; + padding: 44px 40px; + width: 400px; + position: relative; + box-shadow: 0 24px 64px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.03); + animation: cardIn 0.5s cubic-bezier(0.22, 1, 0.36, 1); + } + @keyframes cardIn { + from { opacity: 0; transform: translateY(16px) scale(0.98); } + to { opacity: 1; transform: translateY(0) scale(1); } } .card::before { - content: ''; position: absolute; top: 0; left: 40px; right: 40px; height: 2px; - background: linear-gradient(90deg, #3b82f6, #60a5fa, #a78bfa); + content: ''; position: absolute; top: 0; left: 32px; right: 32px; height: 2px; + background: linear-gradient(90deg, #4f8cff, #7eb3ff, #b794ff); border-radius: 0 0 4px 4px; } - .brand { display: flex; align-items: center; gap: 12px; margin-bottom: 32px; } + .brand { display: flex; align-items: center; gap: 14px; margin-bottom: 36px; } .brand-mark { - width: 40px; height: 40px; background: #3b82f6; border-radius: 10px; + width: 42px; height: 42px; + background: linear-gradient(135deg, #4f8cff, #6366f1); + border-radius: 12px; display: flex; align-items: center; justify-content: center; - font-family: 'Geist Mono', monospace; font-weight: 500; font-size: 13px; color: #fff; + font-family: 'Geist Mono', monospace; font-weight: 600; font-size: 13px; color: #fff; + box-shadow: 0 4px 16px rgba(79,140,255,0.35); } - .brand-name { font-size: 18px; font-weight: 700; } - .brand-sub { font-size: 11px; color: #4a5568; font-family: 'Geist Mono', monospace; letter-spacing: 0.08em; } - label { display: block; font-size: 11px; font-weight: 600; color: #8892a4; letter-spacing: 0.1em; margin-bottom: 7px; } + .brand-name { font-size: 19px; font-weight: 700; letter-spacing: -0.02em; } + .brand-sub { font-size: 9px; color: #525d72; font-family: 'Geist Mono', monospace; letter-spacing: 0.14em; margin-top: 2px; } + label { display: block; font-size: 10px; font-weight: 700; color: #8b95aa; letter-spacing: 0.12em; margin-bottom: 8px; } input[type="password"] { - width: 100%; padding: 11px 14px; background: #181c24; - border: 1px solid #262d3d; border-radius: 10px; + width: 100%; padding: 12px 16px; background: #141820; + border: 1px solid #252d42; border-radius: 12px; font-size: 14px; font-family: 'Geist Mono', monospace; - color: #e8ecf4; letter-spacing: 0.1em; margin-bottom: 20px; - transition: border-color 0.18s; + color: #eef1f8; letter-spacing: 0.08em; margin-bottom: 22px; + transition: border-color 0.22s, box-shadow 0.22s; + } + input[type="password"]:focus { + outline: none; border-color: #4f8cff; + box-shadow: 0 0 0 3px rgba(79,140,255,0.15); } - input[type="password"]:focus { outline: none; border-color: #3b82f6; } button { - width: 100%; padding: 12px; background: #3b82f6; color: #fff; - border: none; border-radius: 10px; font-size: 14px; font-weight: 600; - font-family: 'Syne', sans-serif; cursor: pointer; transition: background 0.18s; + width: 100%; padding: 13px; + background: linear-gradient(135deg, #4f8cff, #6366f1); + color: #fff; border: none; border-radius: 12px; + font-size: 14px; font-weight: 700; + font-family: 'Syne', sans-serif; cursor: pointer; + transition: filter 0.22s, transform 0.15s, box-shadow 0.22s; + box-shadow: 0 4px 18px rgba(79,140,255,0.35); } - button:hover { background: #2563eb; } + button:hover { filter: brightness(1.08); transform: translateY(-1px); box-shadow: 0 6px 24px rgba(79,140,255,0.45); } + button:active { transform: translateY(0); } .error { - color: #ef4444; font-size: 12px; margin-bottom: 14px; - padding: 9px 12px; background: rgba(239,68,68,0.08); - border: 1px solid rgba(239,68,68,0.2); border-radius: 8px; + color: #f05252; font-size: 12px; margin-bottom: 16px; + padding: 10px 14px; background: rgba(240,82,82,0.08); + border: 1px solid rgba(240,82,82,0.22); border-radius: 10px; } diff --git a/platform/templates/pages/dashboard.html b/platform/templates/pages/dashboard.html index 7712cac..9651d68 100644 --- a/platform/templates/pages/dashboard.html +++ b/platform/templates/pages/dashboard.html @@ -1,88 +1,37 @@ {% extends "base.html" %} {% block content %} - -
-
CPU USAGE
+
+
CPU USAGE
+
+
{{ system.cpu_pct or 'โ€ฆ' }}%
-
MEMORY
-
{{ system.memory or 'โ€ฆ' }}
+
+
MEMORY
+
+
+
{{ system.memory or 'โ€ฆ' }}
-
DISK /
-
{{ system.disk or 'โ€ฆ' }}
+
+
DISK /
+
+
+
{{ system.disk or 'โ€ฆ' }}
-
LOAD AVG
-
{{ system.load or 'โ€ฆ' }}
+
+
LOAD AVG
+
+
+
{{ system.load or 'โ€ฆ' }}
@@ -161,8 +110,10 @@ datasets: [{ data: values, backgroundColor: colors, - borderWidth: 0, - hoverOffset: 4, + borderWidth: 2, + borderColor: isDark() ? '#0e1017' : '#ffffff', + hoverOffset: 8, + hoverBorderWidth: 0, }], }, options: { diff --git a/scripts/backup-myapps.sh b/scripts/backup-myapps.sh index 1c287d3..6e5f88c 100755 --- a/scripts/backup-myapps.sh +++ b/scripts/backup-myapps.sh @@ -2,6 +2,8 @@ # ============================================= # backup-myapps.sh โ€” Run on MAIN SERVER # Backs up: Frappe, Nextcloud, Mautic, n8n, Odoo +# + CI/CD (my-jenkins, management-platform) +# + Infrastructure (K8s ingress, Traefik, SSL certs) # Storage tiers: # 1. Local โ†’ /root/backups/ (CRITICAL โ€” always runs) # 2. VM โ†’ SSH tunnel โ†’ /backups/main-server/ @@ -65,6 +67,8 @@ log_status() { echo "=========================================" echo "๐Ÿ“ฆ Starting Backup: $BACKUP_NAME" echo " Apps: Frappe, Nextcloud, Mautic, n8n, Odoo" +echo " CI/CD: my-jenkins, management-platform" +echo " Infra: K8s ingress, Traefik, SSL certs" echo " Tiers: Local โ†’ VM โ†’ โ˜ Cloudflare R2" echo "=========================================" @@ -168,7 +172,85 @@ docker exec frappe-erpnext \ || echo " โญ๏ธ Frappe config not found (skipping)" # -------------------------------------------------- -# 6. Metadata + checksum +# 6. CI/CD & Infrastructure configs โ† NEW SECTION +# -------------------------------------------------- +echo "" +echo "๐Ÿš€ [6b/7] Backing up CI/CD & Infrastructure configs..." +mkdir -p cicd-configs/k8s + +# my-cicd folder (Jenkinsfile, docker-compose for my-jenkins) +if [ -d /root/my-cicd ]; then + cp -r /root/my-cicd cicd-configs/my-cicd \ + && echo " โœ… /root/my-cicd (Jenkinsfile + compose)" \ + || echo " โš ๏ธ my-cicd copy failed" +else + echo " โญ๏ธ /root/my-cicd not found โ€” skipping" +fi + +# my-jenkins Docker volume (jobs, credentials, plugins, build history) +JENKINS_VOL="my-cicd_jenkins_home" +# Try common volume name variants +for VOL_NAME in "my-cicd_jenkins_home" "my-cicd_jenkins-home" "my-cicd_jenkins-data" "jenkins_home"; do + if docker volume inspect "$VOL_NAME" &>/dev/null; then + echo -n " ๐Ÿ“ my-jenkins volume ($VOL_NAME) ... " + docker run --rm \ + -v "${VOL_NAME}:/source:ro" \ + -v "$(pwd)/cicd-configs:/backup" \ + alpine tar czf "/backup/my-jenkins-volume.tar.gz" -C /source . \ + && echo "โœ…" || echo "โš ๏ธ FAILED (continuing)" + break + fi +done + +# management-platform source (excluding pycache) +if [ -d /root/management-platform ]; then + tar czf cicd-configs/management-platform.tar.gz \ + --exclude='*/__pycache__' \ + --exclude='*.pyc' \ + --exclude='*/venv' \ + -C /root management-platform \ + && echo " โœ… management-platform source" \ + || echo " โš ๏ธ management-platform backup failed" +else + echo " โญ๏ธ /root/management-platform not found โ€” skipping" +fi + +# Traefik config folder +if [ -d /root/traefik ]; then + cp -r /root/traefik cicd-configs/traefik \ + && echo " โœ… /root/traefik config" \ + || echo " โš ๏ธ traefik copy failed" +fi + +# acme.json SSL cert backup (critical for domain SSL) +if [ -f /root/acme.json.working-backup ]; then + cp /root/acme.json.working-backup cicd-configs/acme.json.backup \ + && echo " โœ… acme.json (SSL certs)" \ + || echo " โš ๏ธ acme.json copy failed" +fi + +# K8s ingress rules (domain routing config โ€” cloudops, nextcloud, odoo) +kubectl get ingress --all-namespaces -o yaml \ + > cicd-configs/k8s/ingress-all.yaml 2>/dev/null \ + && echo " โœ… K8s ingress rules" \ + || echo " โญ๏ธ kubectl not available โ€” skipping K8s exports" + +# K8s services & endpoints (ExternalName services pointing to Docker containers) +kubectl get services --all-namespaces -o yaml \ + > cicd-configs/k8s/services-all.yaml 2>/dev/null || true +kubectl get endpoints --all-namespaces -o yaml \ + > cicd-configs/k8s/endpoints-all.yaml 2>/dev/null || true + +# K8s TLS secrets (cert-manager issued certs for domains) +kubectl get secret --all-namespaces \ + --field-selector type=kubernetes.io/tls -o yaml \ + > cicd-configs/k8s/tls-secrets.yaml 2>/dev/null \ + && echo " โœ… K8s TLS secrets (domain certs)" || true + +echo " โœ… CI/CD & Infrastructure backup done" + +# -------------------------------------------------- +# 7. Metadata + checksum # -------------------------------------------------- echo "" echo "๐Ÿ“ [6/7] Writing backup metadata..." @@ -179,6 +261,8 @@ Backup Name: $BACKUP_NAME Backup Date: $(date) Hostname: $(hostname) Apps: Frappe, Nextcloud, Mautic, n8n, Odoo +CI/CD: my-jenkins, management-platform +Infra: K8s ingress, Traefik, SSL certs Volumes: $VOLUME_COUNT volume(s) backed up Docker info: $(docker --version 2>/dev/null || echo 'N/A') Storage Tiers: @@ -198,7 +282,7 @@ done echo " โœ… Done" # -------------------------------------------------- -# 7. Compress +# 8. Compress # -------------------------------------------------- echo "" echo "๐Ÿ—œ๏ธ [7/7] Compressing backup..." @@ -223,7 +307,7 @@ echo "" echo "โœ… [TIER 1] Local backup complete: $BACKUP_ARCHIVE ($COMPRESSED_SIZE)" # -------------------------------------------------- -# 8. Retention โ€” keep only MAX_BACKUPS locally +# 9. Retention โ€” keep only MAX_BACKUPS locally # -------------------------------------------------- echo "" echo "๐Ÿงน [Retention] Keeping latest ${MAX_BACKUPS} local backups..." @@ -242,7 +326,7 @@ else fi # -------------------------------------------------- -# 9. VM transfer [TIER 2] โ€” non-fatal +# 10. VM transfer [TIER 2] โ€” non-fatal # -------------------------------------------------- echo "" echo "๐Ÿ“ค [TIER 2] Sending backup to VM (${VM_HOST}:${VM_PORT})..." @@ -266,7 +350,7 @@ else fi # -------------------------------------------------- -# 10. Cloudflare R2 [TIER 3] โ€” non-fatal +# 11. Cloudflare R2 [TIER 3] โ€” non-fatal # -------------------------------------------------- echo "" echo "โ˜๏ธ [TIER 3] Uploading to Cloudflare R2..." @@ -376,7 +460,7 @@ PYEOF fi # -------------------------------------------------- -# 11. Final summary + log +# 12. Final summary + log # -------------------------------------------------- echo "" echo "========================================="