Change : UI/UX design

This commit is contained in:
2026-06-21 18:56:18 +01:00
parent b43c87de73
commit ae267a6d9b
6 changed files with 765 additions and 276 deletions

View File

@@ -158,6 +158,55 @@ container_is_healthy() {
return 1 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 # STEP 1: Restore Volumes
# -------------------------------------------------- # --------------------------------------------------
@@ -259,6 +308,14 @@ declare -A APP_KEY=(
["n8n"]="n8n" ["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 if [ -d "$SCRIPT_DIR/compose-files" ]; then
cd "$SCRIPT_DIR/compose-files" cd "$SCRIPT_DIR/compose-files"
for app in Frappe Odoo Nextcloud Mautic n8n; do for app in Frappe Odoo Nextcloud Mautic n8n; do
@@ -292,6 +349,14 @@ if [ -d "$SCRIPT_DIR/compose-files" ]; then
fi fi
docker-compose up -d 2>&1 | tail -3 docker-compose up -d 2>&1 | tail -3
cd .. 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 done
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
else else
@@ -475,7 +540,15 @@ else
docker start n8n-app 2>/dev/null && echo " ✅ Started n8n-app" || echo " ⚠️ Could not start n8n" docker start n8n-app 2>/dev/null && echo " ✅ Started n8n-app" || echo " ⚠️ Could not start n8n"
fi fi
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 fi
# -------------------------------------------------- # --------------------------------------------------
@@ -485,9 +558,19 @@ echo ""
echo "=========================================" echo "========================================="
echo "✅ RESTORE COMPLETE" echo "✅ RESTORE COMPLETE"
echo "=========================================" 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 summarize_app() {
if app_selected odoo; then echo " Odoo → https://odooo.nav.ovh/web"; fi local label="$1" url="$2" db_ctr="$3"
if app_selected n8n; then echo " n8n → http://${VM_IP}:5678"; fi if [ -n "$db_ctr" ] && ! container_is_healthy "$db_ctr"; then
if app_selected frappe; then echo " Frappe → http://${VM_IP}:8080"; fi 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 "=========================================" echo "========================================="

View File

@@ -9,52 +9,55 @@
/* ── DARK THEME (default) ──────────────────────────────────── */ /* ── DARK THEME (default) ──────────────────────────────────── */
:root { :root {
--bg: #0a0b0e; --bg: #07080c;
--surface: #111318; --surface: #0e1017;
--surface2: #181c24; --surface2: #141820;
--border: #1e2330; --border: #1a2030;
--border2: #262d3d; --border2: #252d42;
--text: #e8ecf4; --text: #eef1f8;
--text2: #8892a4; --text2: #8b95aa;
--text3: #4a5568; --text3: #525d72;
--accent: #3b82f6; --accent: #4f8cff;
--accent2: #60a5fa; --accent2: #7eb3ff;
--green: #22c55e; --green: #2dd47a;
--red: #ef4444; --red: #f05252;
--yellow: #f59e0b; --yellow: #f5b942;
--purple: #a78bfa; --purple: #b794ff;
--cyan: #22d3ee; --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: 12px;
--radius-lg: 16px; --radius-lg: 18px;
--radius-xl: 22px;
--font: 'Syne', sans-serif; --font: 'Syne', sans-serif;
--mono: 'Geist Mono', monospace; --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); --bg2: var(--surface);
--bg3: var(--surface2); --bg3: var(--surface2);
--bg4: #1e2535; --bg4: #1a2235;
--purple-d: #6366f1; --purple-d: #6366f1;
--sidebar-w: 220px; --sidebar-w: 236px;
--sans: 'Syne', sans-serif; --sans: 'Syne', sans-serif;
} }
/* ── LIGHT THEME ───────────────────────────────────────────── */ /* ── LIGHT THEME ───────────────────────────────────────────── */
[data-theme="light"] { [data-theme="light"] {
--bg: #f0f2f7; --bg: #eef1f8;
--surface: #ffffff; --surface: #ffffff;
--surface2: #f5f7fb; --surface2: #f4f6fb;
--border: #dde1ed; --border: #dde3ef;
--border2: #c8cfe0; --border2: #c5cfe0;
--text: #0f1117; --text: #0c0e14;
--text2: #4a5578; --text2: #4a5578;
--text3: #9aa3b8; --text3: #8b95aa;
--accent: #2563eb; --accent: #2563eb;
--accent2: #3b82f6; --accent2: #3b82f6;
@@ -64,11 +67,14 @@
--purple: #7c3aed; --purple: #7c3aed;
--cyan: #0891b2; --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); --bg2: var(--surface);
--bg3: var(--surface2); --bg3: var(--surface2);
--bg4: #edf2fb; --bg4: #e8edf8;
} }
/* ── RESET ─────────────────────────────────────────────────── */ /* ── RESET ─────────────────────────────────────────────────── */
@@ -82,29 +88,53 @@ html, body {
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
-webkit-font-smoothing: antialiased; -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-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 ────────────────────────────────────────────────── */
.layout { display: flex; min-height: 100vh; } .layout { display: flex; min-height: 100vh; position: relative; z-index: 1; }
/* ── SIDEBAR ───────────────────────────────────────────────── */ /* ── SIDEBAR ───────────────────────────────────────────────── */
.sidebar { .sidebar {
width: var(--sidebar-w); width: var(--sidebar-w);
min-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); border-right: 1px solid var(--border);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: fixed; position: fixed;
top: 0; left: 0; bottom: 0; top: 0; left: 0; bottom: 0;
z-index: 100; z-index: 100;
padding: 20px 12px; padding: 22px 14px;
transition: background 0.3s, border-color 0.3s; transition: background 0.35s, border-color 0.35s;
}
[data-theme="light"] .sidebar {
background: rgba(255,255,255,0.92);
} }
.sidebar-brand, .sidebar-brand,
@@ -120,15 +150,16 @@ a.sidebar-brand-link {
} }
.brand-mark { .brand-mark {
width: 36px; height: 36px; width: 38px; height: 38px;
background: var(--accent); background: linear-gradient(135deg, #4f8cff 0%, #6366f1 100%);
border-radius: 8px; border-radius: 11px;
display: flex; align-items: center; justify-content: center; 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; 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-name { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
.brand-sub { font-size: 10px; color: var(--text3); font-family: var(--mono); letter-spacing: 0.08em; } .brand-sub { font-size: 9px; color: var(--text3); font-family: var(--mono); letter-spacing: 0.14em; margin-top: 1px; }
/* nav */ /* nav */
.nav { flex: 1; display: flex; flex-direction: column; gap: 2px; overflow-y: auto; } .nav { flex: 1; display: flex; flex-direction: column; gap: 2px; overflow-y: auto; }
@@ -140,23 +171,38 @@ a.sidebar-brand-link {
} }
.nav-item { .nav-item {
display: flex; align-items: center; gap: 10px; 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; color: var(--text2); text-decoration: none;
font-size: 13px; font-weight: 500; cursor: pointer; font-size: 13px; font-weight: 500; cursor: pointer;
transition: background var(--trans), color var(--trans); transition: background var(--trans), color var(--trans), transform var(--trans), box-shadow var(--trans);
margin-bottom: 1px; 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 span { flex: 1; }
.nav-item:hover { background: var(--surface2); color: var(--text); } .nav-item:hover { background: var(--surface2); color: var(--text); transform: translateX(2px); }
.nav-item.active { background: var(--accent); color: #fff; } .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 { .nav-badge {
margin-left: auto; font-family: var(--mono); font-size: 10px; margin-left: auto; font-family: var(--mono); font-size: 10px;
background: var(--border2); color: var(--text2); background: var(--bg4); color: var(--text2);
padding: 2px 6px; border-radius: 20px; padding: 2px 7px; border-radius: 20px;
border: 1px solid var(--border); 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 */
.sidebar-footer { .sidebar-footer {
@@ -167,8 +213,9 @@ a.sidebar-brand-link {
.status-pill { .status-pill {
flex: 1; display: flex; align-items: center; gap: 7px; flex: 1; display: flex; align-items: center; gap: 7px;
background: var(--surface2); border-radius: 20px; 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; font-size: 11px; color: var(--text2); overflow: hidden;
border: 1px solid var(--border);
} }
.pulse-dot { .pulse-dot {
width: 7px; height: 7px; border-radius: 50%; width: 7px; height: 7px; border-radius: 50%;
@@ -194,14 +241,18 @@ a.sidebar-brand-link {
/* theme toggle */ /* theme toggle */
.theme-toggle { .theme-toggle {
width: 32px; height: 32px; width: 34px; height: 34px;
display: flex; align-items: center; justify-content: center; 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); border: 1px solid var(--border); color: var(--text2);
cursor: pointer; font-size: 14px; 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 ──────────────────────────────────────────────────── */
.main { .main {
@@ -214,109 +265,203 @@ a.sidebar-brand-link {
/* ── TOPBAR ────────────────────────────────────────────────── */ /* ── TOPBAR ────────────────────────────────────────────────── */
.topbar { .topbar {
display: flex; align-items: center; justify-content: space-between; display: flex; align-items: center; justify-content: space-between;
padding: 18px 28px; border-bottom: 1px solid var(--border); padding: 20px 32px;
background: var(--surface); flex-shrink: 0; 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; 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; } [data-theme="light"] .topbar { background: rgba(255,255,255,0.82); }
.page-title { font-size: 20px; font-weight: 700; } .topbar-left { display: flex; align-items: baseline; gap: 14px; }
.page-title { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
.page-subtitle, .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; } .topbar-right { display: flex; align-items: center; gap: 10px; }
.icon-btn { .icon-btn {
width: 34px; height: 34px; background: var(--surface2); width: 36px; height: 36px; background: var(--surface2);
border: 1px solid var(--border); border-radius: 8px; border: 1px solid var(--border); border-radius: 10px;
color: var(--text2); cursor: pointer; color: var(--text2); cursor: pointer;
display: flex; align-items: center; justify-content: center; 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 i,
.icon-btn .spinning { animation: spin 0.8s linear infinite; } .icon-btn .spinning { animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } } @keyframes spin { to { transform: rotate(360deg); } }
.uptime-chip { .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); 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 ───────────────────────────────────────────────── */
.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 ─────────────────────────────────────────────────── */ /* ── CARDS ─────────────────────────────────────────────────── */
.card { .card {
background: var(--surface); border: 1px solid var(--border); background: var(--surface);
border-radius: var(--radius-lg); padding: 20px; border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 22px 24px;
box-shadow: var(--shadow); box-shadow: var(--shadow);
transition: background 0.3s, border-color 0.3s; transition: background 0.35s, border-color 0.35s, box-shadow var(--trans);
margin-bottom: 0; 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 { .card-header {
display: flex; align-items: center; justify-content: space-between; display: flex; align-items: center; justify-content: space-between;
margin-bottom: 16px; margin-bottom: 18px;
} }
.card-title { .card-title {
font-size: 13px; font-weight: 600; letter-spacing: 0.04em; font-size: 13px; font-weight: 700; letter-spacing: 0.06em;
display: flex; align-items: center; gap: 8px; color: var(--text); 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, .card-meta,
.badge-chip { .badge-chip {
font-family: var(--mono); font-size: 11px; color: var(--text3); font-family: var(--mono); font-size: 11px; color: var(--text3);
background: var(--surface2); border: 1px solid var(--border); background: var(--surface2); border: 1px solid var(--border);
padding: 3px 8px; border-radius: 20px; padding: 4px 10px; border-radius: 20px;
} }
/* ── METRICS ROW ───────────────────────────────────────────── */ /* ── METRICS ROW ───────────────────────────────────────────── */
.metrics-row { .metrics-row {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 14px; gap: 16px;
} }
.metric-card { .metric-card {
background: var(--surface2); border: 1px solid var(--border); background: var(--surface);
border-radius: var(--radius); padding: 16px 18px; border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 20px 22px;
position: relative; overflow: hidden; 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 { .metric-card::before {
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
background: var(--accent); 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(2)::before { background: linear-gradient(90deg, var(--purple), #c4b5fd); }
.metric-card:nth-child(3)::before { background: var(--cyan); } .metric-card:nth-child(3)::before { background: linear-gradient(90deg, var(--cyan), #67e8f9); }
.metric-card:nth-child(4)::before { background: var(--yellow); } .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 { .metric-label {
font-family: var(--mono); font-size: 9px; font-weight: 700; 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 { .metric-value {
font-family: var(--mono); font-size: 26px; font-weight: 700; font-family: var(--mono); font-size: 28px; font-weight: 700;
letter-spacing: -0.02em; color: var(--text); letter-spacing: -0.03em; color: var(--text);
line-height: 1; margin-bottom: 10px; 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; } .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-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 { .gauge-fill {
height: 100%; border-radius: 4px; height: 100%; border-radius: 99px;
background: var(--accent); background: linear-gradient(90deg, var(--accent), var(--accent2));
transition: width 0.6s ease; max-width: 100%; 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(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: var(--cyan); } .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: var(--yellow); } .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 ──────────────────────────────────────────────── */ /* ── STAT ROW ──────────────────────────────────────────────── */
/* 173 uses stat-box; 83 uses stat-card — support both */ /* 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 { .stat-box {
flex: 1; text-align: center; 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-lbl { font-size: 10px; color: var(--text3); letter-spacing: 0.06em; }
.stat-row .stat-card { .stat-row .stat-card {
flex: 1; text-align: center; text-align: center;
background: var(--surface2); border: 1px solid var(--border); background: var(--surface2);
border-radius: var(--radius); padding: 14px 16px; 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 ────────────────────────────────────────────────── */ /* ── TABLES ────────────────────────────────────────────────── */
.table-wrap, .table-wrap,
@@ -352,19 +524,32 @@ a.sidebar-brand-link {
.data-table th, .data-table th,
.ct-table th { .ct-table th {
text-align: left; text-align: left;
font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 0.1em; font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 0.12em;
color: var(--text3); padding: 6px 12px 10px; text-transform: uppercase;
color: var(--text3); padding: 8px 14px 12px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
background: var(--surface2);
} }
.data-table td, .data-table td,
.ct-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; vertical-align: middle;
} }
.data-table tr:last-child td, .data-table tr:last-child td,
.ct-table tr:last-child td { border-bottom: none; } .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, .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-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; } .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 ───────────────────────────────────────────────── */ /* ── BUTTONS ───────────────────────────────────────────────── */
.btn { .btn {
display: inline-flex; align-items: center; gap: 7px; 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); font-size: 13px; font-weight: 600; font-family: var(--font);
cursor: pointer; border: none; text-decoration: none; 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: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-primary, .btn.primary {
.btn-danger, .btn.danger { background: rgba(239,68,68,0.15); color: var(--red); border: 1px solid rgba(239,68,68,0.3); } background: linear-gradient(135deg, var(--accent) 0%, #6366f1 100%);
.btn-success { background: var(--green); color: #fff; } color: #fff;
.btn-ghost, .btn.ghost { background: var(--surface2); border: 1px solid var(--border2); color: var(--text2); } box-shadow: 0 4px 14px rgba(79,140,255,0.3);
.btn-ghost:hover, .btn.ghost:hover { background: var(--border2); color: var(--text); filter: none; } }
.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-sm, .btn.sm { padding: 5px 11px; font-size: 12px; }
.btn-lg, .btn.lg { padding: 12px 24px; font-size: 14px; } .btn-lg, .btn.lg { padding: 12px 24px; font-size: 14px; }
@@ -468,11 +670,14 @@ a.sidebar-brand-link {
} }
.form-input { .form-input {
background: var(--surface2); border: 1px solid var(--border2); 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%; 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-input[readonly] { opacity: 0.6; cursor: default; }
.form-row { display: flex; gap: 12px; } .form-row { display: flex; gap: 12px; }
.form-row .form-group { flex: 1; } .form-row .form-group { flex: 1; }
@@ -487,12 +692,17 @@ a.sidebar-brand-link {
.radio-card input { display: none; } .radio-card input { display: none; }
.radio-body { .radio-body {
display: flex; align-items: center; gap: 10px; 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); 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.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); } [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-icon { font-size: 16px; color: var(--accent2); }
.radio-label { font-size: 13px; font-weight: 600; } .radio-label { font-size: 13px; font-weight: 600; }
@@ -549,16 +759,23 @@ a.sidebar-brand-link {
gap: 12px; gap: 12px;
} }
.user-card { .user-card {
padding: 16px; border-radius: 12px; padding: 18px; border-radius: var(--radius-lg);
background: var(--surface2); border: 1px solid var(--border); background: var(--surface);
border: 1px solid var(--border);
cursor: pointer; 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 { .user-card.selected {
border-color: var(--accent); border-color: rgba(79,140,255,0.45);
background: rgba(59,130,246,0.08); background: rgba(79,140,255,0.07);
box-shadow: 0 0 0 1px rgba(59,130,246,0.15); 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); } [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; } .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); margin-bottom: 10px; font-size: 12px; font-weight: 600; color: var(--text2);
} }
.log-console { .log-console {
background: #05060a; border: 1px solid var(--border); background: linear-gradient(180deg, #040508 0%, #080a10 100%);
border-radius: var(--radius); padding: 14px 16px; border: 1px solid var(--border);
font-family: var(--mono); font-size: 12px; line-height: 1.7; border-radius: var(--radius);
max-height: 400px; overflow-y: auto; color: #8892a4; 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; 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; } [data-theme="light"] .log-console { background: #1a1f2e; border-color: #2d3555; }
.log-line { margin: 0; } .log-line { margin: 0; }
@@ -619,9 +839,15 @@ a.sidebar-brand-link {
.backup-list { display: flex; flex-direction: column; gap: 8px; } .backup-list { display: flex; flex-direction: column; gap: 8px; }
.backup-item { .backup-item {
display: flex; align-items: center; justify-content: space-between; 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; background: var(--surface2); gap: 10px; flex-wrap: wrap;
border: 1px solid var(--border); 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-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; } .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 ─────────────────────────────────────────────────── */
.modal-overlay { .modal-overlay {
position: fixed; inset: 0; z-index: 1000; position: fixed; inset: 0; z-index: 1000;
background: rgba(0,0,0,0.65); background: rgba(4,5,8,0.78);
backdrop-filter: blur(3px); backdrop-filter: blur(10px) saturate(1.2);
-webkit-backdrop-filter: blur(10px) saturate(1.2);
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
padding: 20px; animation: fadeIn 0.15s ease; padding: 20px; animation: fadeIn 0.2s ease;
} }
.modal-box { .modal-box {
background: #111318; border: 1px solid #1e2330; background: var(--surface);
border-radius: 16px; width: 100%; max-width: 600px; border: 1px solid var(--border2);
border-radius: var(--radius-xl);
width: 100%; max-width: 600px;
max-height: 88vh; overflow-y: auto; padding: 24px; 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; 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 { .modal-header {
display: flex; align-items: center; justify-content: space-between; 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; } [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: #e8ecf4; } .modal-title { font-size: 15px; font-weight: 700; display: flex; align-items: center; gap: 8px; color: var(--text); }
[data-theme="light"] .modal-title { color: #0f1117; } [data-theme="light"] .modal-title { color: var(--text); }
.modal-close { .modal-close {
background: none; border: none; cursor: pointer; color: #4a5568; background: var(--surface2); border: 1px solid var(--border);
font-size: 18px; line-height: 1; padding: 6px 10px; border-radius: 8px; cursor: pointer; color: var(--text3);
transition: background 0.15s, color 0.15s; 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-close:hover { background: rgba(240,82,82,0.12); color: var(--red); border-color: rgba(240,82,82,0.25); }
.modal-body { padding: 20px 24px 24px; } .modal-body { padding: 4px 0 0; }
.audit-footer { .audit-footer {
display: flex; gap: 8px; margin-top: 18px; padding-top: 14px; 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 ────────────────────────────────────────────────── */
.notice { .notice {
@@ -686,11 +923,15 @@ a.sidebar-brand-link {
/* ── EMPTY STATE ───────────────────────────────────────────── */ /* ── EMPTY STATE ───────────────────────────────────────────── */
.empty-state, .empty-state,
.empty { .empty {
text-align: center; padding: 36px; color: var(--text3); text-align: center; padding: 40px 24px; color: var(--text3);
font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 8px; 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-state i { font-size: 28px; opacity: 0.3; color: var(--text3); }
.empty.warn { color: var(--yellow); } .empty.warn { color: var(--yellow); border-color: rgba(245,185,66,0.25); background: rgba(245,185,66,0.04); }
/* ── SETTINGS ──────────────────────────────────────────────── */ /* ── SETTINGS ──────────────────────────────────────────────── */
.settings-grid { display: flex; flex-direction: column; gap: 0; } .settings-grid { display: flex; flex-direction: column; gap: 0; }
@@ -712,7 +953,10 @@ a.sidebar-brand-link {
/* ── UTILITIES ─────────────────────────────────────────────── */ /* ── UTILITIES ─────────────────────────────────────────────── */
.row-gap { display: flex; align-items: center; gap: 8px; } .row-gap { display: flex; align-items: center; gap: 8px; }
.section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } .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 ─────────────────────────────────────────────── */ /* ── ANIMATION ─────────────────────────────────────────────── */
@keyframes fadeIn { @keyframes fadeIn {
@@ -722,3 +966,101 @@ a.sidebar-brand-link {
/* manual backup wrapper */ /* manual backup wrapper */
#manual-backup-wrapper { border-top: 1px solid var(--border); padding-top: 16px; } #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; }
}

View File

@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=Geist+Mono:wght@300;400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=Geist+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}?v=ui3">
</head> </head>
<body> <body>
<div class="layout"> <div class="layout">

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -10,53 +10,82 @@
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { body {
font-family: 'Syne', sans-serif; font-family: 'Syne', sans-serif;
background: #0a0b0e; background: #07080c;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
min-height: 100vh; color: #e8ecf4; min-height: 100vh; color: #eef1f8;
-webkit-font-smoothing: antialiased;
} }
body::before { 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: background-image:
linear-gradient(rgba(59,130,246,0.03) 1px, transparent 1px), linear-gradient(rgba(79,140,255,0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(59,130,246,0.03) 1px, transparent 1px); linear-gradient(90deg, rgba(79,140,255,0.025) 1px, transparent 1px);
background-size: 40px 40px; pointer-events: none; background-size: 48px 48px;
} }
.card { .card {
background: #111318; border: 1px solid #1e2330; background: rgba(14,16,23,0.9);
border-radius: 20px; padding: 44px 40px; width: 380px; position: relative; 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 { .card::before {
content: ''; position: absolute; top: 0; left: 40px; right: 40px; height: 2px; content: ''; position: absolute; top: 0; left: 32px; right: 32px; height: 2px;
background: linear-gradient(90deg, #3b82f6, #60a5fa, #a78bfa); background: linear-gradient(90deg, #4f8cff, #7eb3ff, #b794ff);
border-radius: 0 0 4px 4px; 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 { .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; 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-name { font-size: 19px; font-weight: 700; letter-spacing: -0.02em; }
.brand-sub { font-size: 11px; color: #4a5568; font-family: 'Geist Mono', monospace; letter-spacing: 0.08em; } .brand-sub { font-size: 9px; color: #525d72; font-family: 'Geist Mono', monospace; letter-spacing: 0.14em; margin-top: 2px; }
label { display: block; font-size: 11px; font-weight: 600; color: #8892a4; letter-spacing: 0.1em; margin-bottom: 7px; } label { display: block; font-size: 10px; font-weight: 700; color: #8b95aa; letter-spacing: 0.12em; margin-bottom: 8px; }
input[type="password"] { input[type="password"] {
width: 100%; padding: 11px 14px; background: #181c24; width: 100%; padding: 12px 16px; background: #141820;
border: 1px solid #262d3d; border-radius: 10px; border: 1px solid #252d42; border-radius: 12px;
font-size: 14px; font-family: 'Geist Mono', monospace; font-size: 14px; font-family: 'Geist Mono', monospace;
color: #e8ecf4; letter-spacing: 0.1em; margin-bottom: 20px; color: #eef1f8; letter-spacing: 0.08em; margin-bottom: 22px;
transition: border-color 0.18s; 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 { button {
width: 100%; padding: 12px; background: #3b82f6; color: #fff; width: 100%; padding: 13px;
border: none; border-radius: 10px; font-size: 14px; font-weight: 600; background: linear-gradient(135deg, #4f8cff, #6366f1);
font-family: 'Syne', sans-serif; cursor: pointer; transition: background 0.18s; 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 { .error {
color: #ef4444; font-size: 12px; margin-bottom: 14px; color: #f05252; font-size: 12px; margin-bottom: 16px;
padding: 9px 12px; background: rgba(239,68,68,0.08); padding: 10px 14px; background: rgba(240,82,82,0.08);
border: 1px solid rgba(239,68,68,0.2); border-radius: 8px; border: 1px solid rgba(240,82,82,0.22); border-radius: 10px;
} }
</style> </style>
</head> </head>

View File

@@ -1,88 +1,37 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<style>
.dash-charts {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.dash-chart-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 18px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 260px;
}
.dash-chart-card h3 {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.06em;
color: var(--text2);
margin: 0 0 12px;
align-self: flex-start;
display: flex;
align-items: center;
gap: 8px;
}
.dash-chart-card h3 i { color: var(--accent2); font-size: 11px; }
.dash-chart-wrap {
position: relative;
width: 100%;
max-width: 200px;
height: 200px;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.dash-chart-legend {
display: flex;
flex-wrap: wrap;
gap: 10px 16px;
justify-content: center;
margin-top: 12px;
font-family: var(--mono);
font-size: 10px;
color: var(--text2);
}
.dash-chart-legend span { display: inline-flex; align-items: center; gap: 5px; }
.dash-chart-legend .dot { width: 8px; height: 8px; border-radius: 50%; }
.dash-quick-links {
display: flex;
gap: 8px;
margin-top: 10px;
flex-wrap: wrap;
justify-content: center;
}
@media (max-width: 1100px) {
.dash-charts { grid-template-columns: 1fr; }
}
</style>
<div class="metrics-row"> <div class="metrics-row">
<div class="metric-card cpu"> <div class="metric-card cpu">
<div class="metric-label">CPU USAGE</div> <div class="metric-top">
<div class="metric-label">CPU USAGE</div>
<div class="metric-icon"><i class="fas fa-microchip"></i></div>
</div>
<div class="metric-value" id="m-cpu">{{ system.cpu_pct or '…' }}<span>%</span></div> <div class="metric-value" id="m-cpu">{{ system.cpu_pct or '…' }}<span>%</span></div>
<div class="gauge-bar"><div class="gauge-fill" id="g-cpu" style="width:{{ system.cpu_pct or 0 }}%"></div></div> <div class="gauge-bar"><div class="gauge-fill" id="g-cpu" style="width:{{ system.cpu_pct or 0 }}%"></div></div>
</div> </div>
<div class="metric-card mem"> <div class="metric-card mem">
<div class="metric-label">MEMORY</div> <div class="metric-top">
<div class="metric-value" id="m-mem" style="font-size:16px;">{{ system.memory or '…' }}</div> <div class="metric-label">MEMORY</div>
<div class="metric-icon"><i class="fas fa-memory"></i></div>
</div>
<div class="metric-value small" id="m-mem">{{ system.memory or '…' }}</div>
<div class="gauge-bar"><div class="gauge-fill" id="g-mem" style="width:{{ system.mem_pct or 0 }}%"></div></div> <div class="gauge-bar"><div class="gauge-fill" id="g-mem" style="width:{{ system.mem_pct or 0 }}%"></div></div>
</div> </div>
<div class="metric-card disk"> <div class="metric-card disk">
<div class="metric-label">DISK /</div> <div class="metric-top">
<div class="metric-value" id="m-disk" style="font-size:16px;">{{ system.disk or '…' }}</div> <div class="metric-label">DISK /</div>
<div class="metric-icon"><i class="fas fa-hard-drive"></i></div>
</div>
<div class="metric-value small" id="m-disk">{{ system.disk or '…' }}</div>
<div class="gauge-bar"><div class="gauge-fill" id="g-disk" style="width:{{ system.disk_pct or 0 }}%"></div></div> <div class="gauge-bar"><div class="gauge-fill" id="g-disk" style="width:{{ system.disk_pct or 0 }}%"></div></div>
</div> </div>
<div class="metric-card load"> <div class="metric-card load">
<div class="metric-label">LOAD AVG</div> <div class="metric-top">
<div class="metric-value" id="m-load" style="font-size:16px;">{{ system.load or '…' }}</div> <div class="metric-label">LOAD AVG</div>
<div class="metric-icon"><i class="fas fa-gauge-high"></i></div>
</div>
<div class="metric-value small" id="m-load">{{ system.load or '…' }}</div>
<div class="gauge-bar"><div class="gauge-fill" id="g-load" style="width:10%"></div></div> <div class="gauge-bar"><div class="gauge-fill" id="g-load" style="width:10%"></div></div>
</div> </div>
</div> </div>
@@ -161,8 +110,10 @@
datasets: [{ datasets: [{
data: values, data: values,
backgroundColor: colors, backgroundColor: colors,
borderWidth: 0, borderWidth: 2,
hoverOffset: 4, borderColor: isDark() ? '#0e1017' : '#ffffff',
hoverOffset: 8,
hoverBorderWidth: 0,
}], }],
}, },
options: { options: {

View File

@@ -2,6 +2,8 @@
# ============================================= # =============================================
# backup-myapps.sh — Run on MAIN SERVER # backup-myapps.sh — Run on MAIN SERVER
# Backs up: Frappe, Nextcloud, Mautic, n8n, Odoo # Backs up: Frappe, Nextcloud, Mautic, n8n, Odoo
# + CI/CD (my-jenkins, management-platform)
# + Infrastructure (K8s ingress, Traefik, SSL certs)
# Storage tiers: # Storage tiers:
# 1. Local → /root/backups/ (CRITICAL — always runs) # 1. Local → /root/backups/ (CRITICAL — always runs)
# 2. VM → SSH tunnel → /backups/main-server/ # 2. VM → SSH tunnel → /backups/main-server/
@@ -65,6 +67,8 @@ log_status() {
echo "=========================================" echo "========================================="
echo "📦 Starting Backup: $BACKUP_NAME" echo "📦 Starting Backup: $BACKUP_NAME"
echo " Apps: Frappe, Nextcloud, Mautic, n8n, Odoo" 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 " Tiers: Local → VM → ☁ Cloudflare R2"
echo "=========================================" echo "========================================="
@@ -168,7 +172,85 @@ docker exec frappe-erpnext \
|| echo " ⏭️ Frappe config not found (skipping)" || 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 ""
echo "📝 [6/7] Writing backup metadata..." echo "📝 [6/7] Writing backup metadata..."
@@ -179,6 +261,8 @@ Backup Name: $BACKUP_NAME
Backup Date: $(date) Backup Date: $(date)
Hostname: $(hostname) Hostname: $(hostname)
Apps: Frappe, Nextcloud, Mautic, n8n, Odoo 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 Volumes: $VOLUME_COUNT volume(s) backed up
Docker info: $(docker --version 2>/dev/null || echo 'N/A') Docker info: $(docker --version 2>/dev/null || echo 'N/A')
Storage Tiers: Storage Tiers:
@@ -198,7 +282,7 @@ done
echo " ✅ Done" echo " ✅ Done"
# -------------------------------------------------- # --------------------------------------------------
# 7. Compress # 8. Compress
# -------------------------------------------------- # --------------------------------------------------
echo "" echo ""
echo "🗜️ [7/7] Compressing backup..." echo "🗜️ [7/7] Compressing backup..."
@@ -223,7 +307,7 @@ echo ""
echo "✅ [TIER 1] Local backup complete: $BACKUP_ARCHIVE ($COMPRESSED_SIZE)" 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 ""
echo "🧹 [Retention] Keeping latest ${MAX_BACKUPS} local backups..." echo "🧹 [Retention] Keeping latest ${MAX_BACKUPS} local backups..."
@@ -242,7 +326,7 @@ else
fi fi
# -------------------------------------------------- # --------------------------------------------------
# 9. VM transfer [TIER 2] — non-fatal # 10. VM transfer [TIER 2] — non-fatal
# -------------------------------------------------- # --------------------------------------------------
echo "" echo ""
echo "📤 [TIER 2] Sending backup to VM (${VM_HOST}:${VM_PORT})..." echo "📤 [TIER 2] Sending backup to VM (${VM_HOST}:${VM_PORT})..."
@@ -266,7 +350,7 @@ else
fi fi
# -------------------------------------------------- # --------------------------------------------------
# 10. Cloudflare R2 [TIER 3] — non-fatal # 11. Cloudflare R2 [TIER 3] — non-fatal
# -------------------------------------------------- # --------------------------------------------------
echo "" echo ""
echo "☁️ [TIER 3] Uploading to Cloudflare R2..." echo "☁️ [TIER 3] Uploading to Cloudflare R2..."
@@ -376,7 +460,7 @@ PYEOF
fi fi
# -------------------------------------------------- # --------------------------------------------------
# 11. Final summary + log # 12. Final summary + log
# -------------------------------------------------- # --------------------------------------------------
echo "" echo ""
echo "=========================================" echo "========================================="