From 110dfc6dc0a0d61655d66560542622bdbf1f7bba Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 16 Jul 2026 11:33:45 -0500 Subject: [PATCH 1/2] feat: redesign app shell and pages with new left-sidebar UI Move primary navigation from a top navbar to a left sidebar with a slim top utility bar (jobs, cluster switcher, theme toggle), and port every page's markup to the new design-token/component system (card-x, tbl, pill, stat, node-strip, cfg-grid, vnc, etc.) so the whole app shares one consistent look in both light and dark themes. Also: - add the ProxUI logo/favicon assets and wire them into the shell - rename Create VM nav item/route to "Create" (/create) since it creates both VMs and containers - add an adaptive humanize_bytes filter so storage usage shows in the best-fit unit (B-PB) instead of hardcoded GB - restyle VM/CT metrics charts with the design's Chart.js look (gradient fill, hidden gridlines, theme-aware colors) - fix several silently-broken bi-* icon references not present in the pinned Bootstrap Icons version - align the VM detail page's Reset button and Migrate modal confirmation with the rest of the action/dialog patterns --- app.py | 31 +- static/img/favicon.png | Bin 0 -> 5371 bytes static/img/favicon.svg | 10 + static/img/logo-wordmark.svg | 14 + static/img/logo.svg | 14 + static/style.css | 450 +++++++++++++++- templates/base.html | 233 +++++---- templates/cluster.html | 101 ++-- templates/connect.html | 540 ++++++++++---------- templates/create_vm.html | 148 +++--- templates/index.html | 399 ++++++--------- templates/isos_templates.html | 661 +++++++++++++----------- templates/networks.html | 82 ++- templates/storages.html | 135 +++-- templates/tasks.html | 159 +++--- templates/vm_clone.html | 150 +++--- templates/vm_console.html | 50 +- templates/vm_detail.html | 933 ++++++++++++++++------------------ templates/vm_edit.html | 271 +++++----- templates/vms.html | 240 ++++----- tests/test_integration.py | 4 +- 21 files changed, 2492 insertions(+), 2133 deletions(-) create mode 100644 static/img/favicon.png create mode 100644 static/img/favicon.svg create mode 100644 static/img/logo-wordmark.svg create mode 100644 static/img/logo.svg diff --git a/app.py b/app.py index 880ae75..3c6d308 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,6 @@ import base64 import json +import math import os import pprint import re @@ -448,6 +449,30 @@ def humanize_uptime(seconds): app.jinja_env.filters["humanize_uptime"] = humanize_uptime + +def humanize_bytes(num_bytes, decimals=1): + """Convert a byte count to an adaptive-unit string (B/KB/MB/GB/TB/PB), + keeping the numeric value between roughly 1.0 and 999.9.""" + try: + num_bytes = float(num_bytes) + except (ValueError, TypeError): + return "N/A" + if num_bytes <= 0: + return "0 B" + + units = ["B", "KB", "MB", "GB", "TB", "PB"] + k = 1024 + unit_index = int(math.floor(math.log(num_bytes, k))) + unit_index = max(0, min(unit_index, len(units) - 1)) + value = num_bytes / (k**unit_index) + + if unit_index == 0: + return f"{int(value)} B" + return f"{value:.{decimals}f} {units[unit_index]}" + + +app.jinja_env.filters["humanize_bytes"] = humanize_bytes + # Configuration file path for Docker volume import os @@ -2315,6 +2340,8 @@ def index(): # Get node list for metrics nodes = [n["name"] for n in cluster_nodes if n.get("status") == "online"] + total_nodes = len(cluster_nodes) + online_nodes = len(nodes) return render_template( "index.html", @@ -2326,6 +2353,8 @@ def index(): top_cpu=top_cpu, top_memory=top_memory, nodes=nodes, + total_nodes=total_nodes, + online_nodes=online_nodes, ) @@ -2649,7 +2678,7 @@ def tasks(): return render_template("tasks.html") -@app.route("/create_vm", methods=["GET", "POST"]) +@app.route("/create", methods=["GET", "POST"]) def create_vm(): """Create new VM or container""" if request.method == "POST": diff --git a/static/img/favicon.png b/static/img/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..d6fd996ccfb68d3d4b2ee7a79da70861bb0cd2ca GIT binary patch literal 5371 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 Z>HqMassNP!fS3RP002ovPDHLkV1iuCNc;c* literal 0 HcmV?d00001 diff --git a/static/img/favicon.svg b/static/img/favicon.svg new file mode 100644 index 0000000..af3bad1 --- /dev/null +++ b/static/img/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/static/img/logo-wordmark.svg b/static/img/logo-wordmark.svg new file mode 100644 index 0000000..7905d45 --- /dev/null +++ b/static/img/logo-wordmark.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + ProxUI + diff --git a/static/img/logo.svg b/static/img/logo.svg new file mode 100644 index 0000000..22a31c1 --- /dev/null +++ b/static/img/logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/static/style.css b/static/style.css index 3411c8b..7f340ac 100644 --- a/static/style.css +++ b/static/style.css @@ -2,39 +2,81 @@ :root { color-scheme: light; - --app-bg: #f5f7fb; - --app-surface: #ffffff; - --app-surface-alt: #f8f9fa; - --app-surface-elevated: #ffffff; - --app-text: #212529; - --app-muted: #6c757d; - --app-border: #dee2e6; - --app-hover: rgba(13, 110, 253, 0.06); - --app-input-bg: #ffffff; - --app-input-disabled-bg: #e9ecef; - --app-shadow: rgba(0, 0, 0, 0.075); + + /* ── Layout + typography tokens ── */ + --sidebar-w: 264px; --rail-w: 74px; --topbar-h: 60px; + --radius: 12px; --radius-sm: 9px; + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; + + /* ── Design tokens (light) ── */ + --bg:#eef1f6; --bg-elev:#f6f8fb; --panel:#ffffff; --panel-2:#f7f9fc; --panel-hover:#f0f4fa; + --border:#e1e7f0; --border-soft:#eaeef5; + --text:#0f1a2b; --text-muted:#5b6b83; --text-faint:#8b98ad; + --accent:#2f6bff; --accent-soft:rgba(47,107,255,.10); --accent-border:rgba(47,107,255,.30); + --brand-grad:linear-gradient(135deg,#2f6bff,#6f6bff); + --ok:#12a06a; --ok-soft:rgba(18,160,106,.12); + --warn:#c9860a; --warn-soft:rgba(201,134,10,.14); + --danger:#e03e3e; --danger-soft:rgba(224,62,62,.12); + --violet:#6f5bff; --violet-soft:rgba(111,91,255,.12); + --sidebar-bg:#10131c; + --shadow:0 1px 2px rgba(16,28,51,.06),0 10px 30px rgba(16,28,51,.08); --shadow-sm:0 1px 2px rgba(16,28,51,.08); + --grid-line:rgba(15,26,43,.06); --scrim:rgba(15,26,43,.4); + + /* ── Bridge: legacy --app-* vars now resolve to the design tokens above. + Because these use var(), they automatically follow the token overrides in + the dark block below (custom-property substitution is per-element). ── */ + --app-bg: var(--bg); + --app-surface: var(--panel); + --app-surface-alt: var(--panel-2); + --app-surface-elevated: var(--bg-elev); + --app-text: var(--text); + --app-muted: var(--text-muted); + --app-border: var(--border); + --app-hover: var(--panel-hover); + --app-input-bg: var(--panel-2); + --app-input-disabled-bg: var(--border-soft); + --app-shadow: rgba(16, 28, 51, 0.06); } :root[data-theme="dark"] { color-scheme: dark; - --app-bg: #111827; - --app-surface: #172033; - --app-surface-alt: #202a3d; - --app-surface-elevated: #1c2638; - --app-text: #e6eaf0; - --app-muted: #a5afbf; - --app-border: #334155; - --app-hover: rgba(96, 165, 250, 0.12); - --app-input-bg: #101827; - --app-input-disabled-bg: #253044; + + /* ── Design tokens (dark) — override the same names; --app-* follow automatically ── */ + --bg:#0c0f16; --bg-elev:#11151f; --panel:#151a25; --panel-2:#1a2130; --panel-hover:#1f2838; + --border:#232b3a; --border-soft:#1b222e; + --text:#e6ebf5; --text-muted:#93a0b5; --text-faint:#64708a; + --accent:#4c8dff; --accent-soft:rgba(76,141,255,.14); --accent-border:rgba(76,141,255,.35); + --brand-grad:linear-gradient(135deg,#4c8dff,#6f6bff); + --ok:#37d19a; --ok-soft:rgba(55,209,154,.15); + --warn:#f5b544; --warn-soft:rgba(245,181,68,.15); + --danger:#ff6b6b; --danger-soft:rgba(255,107,107,.15); + --violet:#8b7cff; --violet-soft:rgba(139,124,255,.15); + --sidebar-bg:#0b0e15; + --shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px rgba(0,0,0,.28); --shadow-sm:0 1px 2px rgba(0,0,0,.35); + --grid-line:rgba(255,255,255,.05); --scrim:rgba(3,6,12,.66); + --app-shadow: rgba(0, 0, 0, 0.35); } +html, body { height: 100%; } body { + margin: 0; background-color: var(--app-bg); color: var(--app-text); + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + overflow-x: hidden; } +/* Themed scrollbars */ +* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; } +*::-webkit-scrollbar { width: 9px; height: 9px; } +*::-webkit-scrollbar-thumb { background: var(--border); border-radius: 20px; } +*::-webkit-scrollbar-thumb:hover { background: var(--text-faint); } + .navbar-brand { font-weight: bold; } @@ -154,6 +196,7 @@ body { .spin { animation: spin 1s linear infinite; + display: inline-block; } /* Form improvements */ @@ -360,5 +403,366 @@ body { background-size: 20px; } -.spin { animation: spin 1s linear infinite; } -@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } +/* ============================================================ + New UI shell — left sidebar + top utility bar (ported from the + ProxUI redesign mockup). Hook classes: .app-shell / .app-sidebar / + .app-nav-primary / .app-navbar / .app-nav-utility / .app-main. + ============================================================ */ +a { color: var(--accent); text-decoration: none; } +a:hover { color: var(--accent); text-decoration: underline; } +.text-muted-2 { color: var(--text-muted) !important; } +.text-faint { color: var(--text-faint) !important; } +.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; } + +/* ===================== SHELL ===================== */ +.app-shell{ display:grid; grid-template-columns:var(--sidebar-w) 1fr; grid-template-rows:100vh; } + +.app-sidebar{ grid-column:1; background:var(--sidebar-bg); color:#c7d0e0; display:flex; flex-direction:column; + min-height:0; border-right:1px solid rgba(255,255,255,.06); position:relative; z-index:40; } +.sidebar-brand{ display:flex; align-items:center; gap:11px; height:var(--topbar-h); padding:0 20px; flex-shrink:0; + border-bottom:1px solid rgba(255,255,255,.06); text-decoration:none; } +.sidebar-brand:hover{ text-decoration:none; } +.brand-mark{ width:34px; height:34px; flex-shrink:0; display:grid; place-items:center; } +.brand-mark img{ width:100%; height:100%; display:block; } +.brand-text{ display:flex; flex-direction:column; line-height:1.1; min-width:0; } +.brand-name{ font-weight:700; font-size:16px; color:#fff; letter-spacing:-.2px; } +.brand-ver{ font-size:11px; color:#7a8599; font-variant-numeric:tabular-nums; } +.app-nav-primary{ flex:1 1 auto; min-height:0; overflow-y:auto; padding:14px 12px; display:flex; flex-direction:column; gap:3px; list-style:none; margin:0; } +.nav-section-label{ font-size:10.5px; font-weight:700; letter-spacing:.09em; text-transform:uppercase; + color:#5b6579; padding:16px 12px 7px; white-space:nowrap; } +.nav-link-item{ display:flex; align-items:center; gap:12px; padding:9px 12px; border-radius:var(--radius-sm); + color:#aeb8ca; font-weight:500; font-size:13.5px; white-space:nowrap; position:relative; cursor:pointer; + transition:background .13s,color .13s; } +.nav-link-item .bi{ font-size:17px; flex-shrink:0; width:20px; text-align:center; } +.nav-link-item:hover{ background:rgba(255,255,255,.055); color:#fff; text-decoration:none; } +.nav-link-item.active{ background:var(--accent-soft); color:#fff; } +.nav-link-item.active::before{ content:""; position:absolute; left:-12px; top:50%; transform:translateY(-50%); + width:3px; height:22px; border-radius:0 3px 3px 0; background:var(--accent); } +.nav-link-item.disabled{ color:#5f6a7e; cursor:default; } +.nav-link-item.disabled:hover{ background:transparent; color:#5f6a7e; } +.nav-badge{ margin-left:auto; font-size:9.5px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; + padding:2px 7px; border-radius:20px; background:rgba(255,255,255,.08); color:#8a94a8; } +.nav-badge.soon{ background:var(--warn-soft); color:var(--warn); } +.nav-count{ margin-left:auto; font-size:11px; font-weight:700; min-width:20px; text-align:center; padding:1px 6px; + border-radius:20px; background:rgba(255,255,255,.09); color:#c7d0e0; } +.sidebar-foot{ flex-shrink:0; padding:12px; border-top:1px solid rgba(255,255,255,.06); } +.node-pill{ display:flex; align-items:center; gap:10px; padding:9px 11px; border-radius:var(--radius-sm); background:rgba(255,255,255,.04); } +.node-pill .dot{ width:8px; height:8px; border-radius:50%; background:var(--ok); box-shadow:0 0 0 3px var(--ok-soft); flex-shrink:0; } +.node-pill .meta{ min-width:0; line-height:1.25; } +.node-pill .nm{ font-size:12.5px; color:#e6ebf5; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } +.node-pill .sub{ font-size:10.5px; color:#7a8599; white-space:nowrap; } + +.app-col{ grid-column:2; display:flex; flex-direction:column; min-width:0; height:100vh; } + +.app-navbar{ height:var(--topbar-h); flex-shrink:0; display:flex; align-items:center; gap:14px; padding:0 20px; + background:var(--bg-elev); border-bottom:1px solid var(--border); position:sticky; top:0; z-index:30; } +.topbar-left{ display:flex; align-items:center; gap:14px; min-width:0; } +.page-heading{ min-width:0; line-height:1.15; } +.page-heading .crumb{ font-size:11px; color:var(--text-faint); font-weight:500; } +.page-heading .h{ font-size:16px; font-weight:700; letter-spacing:-.3px; white-space:nowrap; } +.icon-btn{ width:38px; height:38px; border-radius:10px; border:1px solid transparent; background:transparent; + color:var(--text-muted); font-size:18px; display:grid; place-items:center; cursor:pointer; position:relative; + transition:background .13s,color .13s,border-color .13s; } +.icon-btn:hover{ background:var(--panel-hover); color:var(--text); } +.icon-btn.on,.icon-btn.show{ background:var(--accent-soft); color:var(--accent); } +.hamburger{ display:none; } +.app-nav-utility{ display:flex; align-items:center; gap:8px; margin-left:auto; } +.count-badge{ position:absolute; top:3px; right:3px; min-width:17px; height:17px; padding:0 4px; border-radius:20px; + background:var(--accent); color:#fff; font-size:10px; font-weight:700; display:grid; place-items:center; + border:2px solid var(--bg-elev); line-height:1; } +.count-badge.warn{ background:var(--warn); } +.util-divider{ width:1px; height:26px; background:var(--border); margin:0 3px; } +.cluster-trigger{ display:flex; align-items:center; gap:10px; height:40px; padding:0 12px; border-radius:10px; + border:1px solid var(--border); background:var(--panel); color:var(--text); cursor:pointer; transition:border-color .13s,background .13s; } +.cluster-trigger:hover{ border-color:var(--accent-border); background:var(--panel-hover); } +.cluster-trigger .cl-ico{ color:var(--accent); font-size:16px; } +.cluster-trigger .cl-name{ font-weight:600; font-size:13px; white-space:nowrap; } +.cluster-trigger .cl-lbl{ font-size:10px; color:var(--text-faint); line-height:1; } +.cluster-trigger .bi-chevron-down{ font-size:11px; color:var(--text-faint); } +.pop{ position:absolute; top:calc(100% + 9px); right:0; min-width:280px; background:var(--panel); + border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); z-index:60; overflow:hidden; } +.pop.left{ right:auto; left:0; } +.pop-head{ display:flex; align-items:center; justify-content:space-between; padding:12px 15px; border-bottom:1px solid var(--border-soft); } +.pop-head .t{ font-weight:700; font-size:13px; } +.pop-body{ max-height:340px; overflow-y:auto; } +.pop-foot{ padding:9px 15px; border-top:1px solid var(--border-soft); } +.pop-item{ display:flex; align-items:center; gap:11px; padding:10px 15px; cursor:pointer; color:var(--text); transition:background .12s; } +.pop-item:hover{ background:var(--panel-hover); text-decoration:none; color:var(--text); } +.pop-item.active-cl{ background:var(--accent-soft); } +.pop-item .job-ico{ width:30px; height:30px; border-radius:8px; display:grid; place-items:center; font-size:15px; flex-shrink:0; } +.job-ico.run{ background:var(--accent-soft); color:var(--accent); } +.job-ico.ok{ background:var(--ok-soft); color:var(--ok); } +.job-ico.err{ background:var(--danger-soft); color:var(--danger); } +.pop-empty{ padding:34px 15px; text-align:center; color:var(--text-faint); font-size:13px; } + +.app-main{ flex:1 1 auto; min-height:0; overflow-y:auto; padding:24px 26px 48px; } +.main-inner{ max-width:1560px; margin:0 auto; } + +/* ===================== SHARED PIECES ===================== */ +.card-x{ background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow-sm); } +.card-x .cx-head{ display:flex; align-items:center; gap:10px; padding:14px 18px; border-bottom:1px solid var(--border-soft); } +.card-x .cx-title{ font-size:13px; font-weight:700; letter-spacing:.01em; } +.card-x .cx-body{ padding:16px 18px; } +.card-x .cx-body.tight{ padding:6px 6px; } +.card-x .cx-body.flush{ padding:0; } + +.stat{ background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); padding:16px 17px; + display:flex; align-items:flex-start; gap:14px; box-shadow:var(--shadow-sm); position:relative; overflow:hidden; + transition:border-color .15s,transform .15s; height:100%; } +.stat:hover{ border-color:var(--accent-border); transform:translateY(-1px); } +.stat .ico{ width:44px; height:44px; border-radius:11px; display:grid; place-items:center; font-size:21px; flex-shrink:0; } +.stat .val{ font-size:27px; font-weight:750; line-height:1; letter-spacing:-.5px; font-variant-numeric:tabular-nums; } +.stat .lbl{ font-size:12px; color:var(--text-muted); font-weight:600; margin-top:5px; } +.stat .trend{ font-size:11px; font-weight:600; margin-top:6px; display:flex; align-items:center; gap:4px; } +.ico.blue{ background:var(--accent-soft); color:var(--accent); } +.ico.ok{ background:var(--ok-soft); color:var(--ok); } +.ico.warn{ background:var(--warn-soft); color:var(--warn); } +.ico.violet{ background:var(--violet-soft); color:var(--violet); } +.ico.slate{ background:rgba(148,163,184,.14); color:var(--text-muted); } +.ico.danger{ background:var(--danger-soft); color:var(--danger); } + +.pill{ display:inline-flex; align-items:center; gap:5px; font-size:11px; font-weight:600; padding:3px 9px; border-radius:20px; line-height:1.3; white-space:nowrap; } +.pill .d{ width:6px; height:6px; border-radius:50%; } +.pill.run{ background:var(--ok-soft); color:var(--ok); } .pill.run .d{ background:var(--ok); box-shadow:0 0 0 3px var(--ok-soft); } +.pill.stop{ background:var(--danger-soft); color:var(--danger); } .pill.stop .d{ background:var(--danger); } +.pill.tmpl{ background:var(--violet-soft); color:var(--violet); } .pill.tmpl .d{ background:var(--violet); } +.pill.maint{ background:var(--warn-soft); color:var(--warn); } .pill.maint .d{ background:var(--warn); } +.pill.err{ background:var(--danger-soft); color:var(--danger); } +.pill.ok{ background:var(--ok-soft); color:var(--ok); } +.pill.info{ background:var(--accent-soft); color:var(--accent); } +.type-chip{ font-size:10px; font-weight:700; letter-spacing:.04em; padding:2px 7px; border-radius:6px; + text-transform:uppercase; background:var(--panel-2); color:var(--text-muted); border:1px solid var(--border); white-space:nowrap; } +.type-chip.vm{ color:var(--accent); border-color:var(--accent-border); } +.type-chip.lxc{ color:var(--violet); border-color:var(--violet); } + +.tbl{ width:100%; border-collapse:collapse; font-size:13px; } +.tbl th{ text-align:left; font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; + color:var(--text-faint); padding:10px 14px; border-bottom:1px solid var(--border); position:sticky; top:0; + background:var(--panel); z-index:1; white-space:nowrap; } +.tbl td{ padding:11px 14px; border-bottom:1px solid var(--border-soft); vertical-align:middle; } +.tbl tbody tr:last-child td{ border-bottom:0; } +.tbl tbody tr{ transition:background .1s; } +.tbl tbody tr:hover{ background:var(--panel-hover); } +.tbl td .nm{ font-weight:600; color:var(--text); } +.tbl td.num{ text-align:right; font-variant-numeric:tabular-nums; } +.tbl th.num{ text-align:right; } + +.ubar{ height:6px; border-radius:20px; background:var(--panel-2); overflow:hidden; min-width:60px; } +.ubar > i{ display:block; height:100%; border-radius:20px; background:var(--accent); } +.ubar > i.hot{ background:var(--warn); } +.ubar > i.crit{ background:var(--danger); } +.ubar > i.okc{ background:var(--ok); } +.usage-cell{ display:flex; align-items:center; gap:9px; } +.usage-cell .pct{ font-size:12px; font-variant-numeric:tabular-nums; min-width:44px; text-align:right; color:var(--text-muted); } + +.seg{ display:inline-flex; background:var(--panel-2); border:1px solid var(--border); border-radius:9px; padding:3px; gap:2px; } +.seg button{ border:0; background:transparent; color:var(--text-muted); font-size:12px; font-weight:600; padding:5px 12px; + border-radius:6px; cursor:pointer; transition:background .12s,color .12s; white-space:nowrap; } +.seg button:hover{ color:var(--text); } +.seg button.on{ background:var(--panel); color:var(--text); box-shadow:var(--shadow-sm); } + +.chart-box{ position:relative; height:150px; } +.chart-box.tall{ height:210px; } +.sec-title{ font-size:15px; font-weight:750; letter-spacing:-.3px; } +.sec-sub{ font-size:12.5px; color:var(--text-muted); } + +/* buttons */ +.btn-x{ display:inline-flex; align-items:center; gap:7px; font-size:13px; font-weight:600; padding:8px 14px; + border-radius:9px; border:1px solid var(--border); background:var(--panel); color:var(--text); cursor:pointer; + transition:background .12s,border-color .12s,color .12s; white-space:nowrap; } +.btn-x:hover{ background:var(--panel-hover); border-color:var(--accent-border); color:var(--text); text-decoration:none; } +.btn-x.primary{ background:var(--accent); border-color:var(--accent); color:#fff; } +.btn-x.primary:hover{ filter:brightness(1.08); color:#fff; } +.btn-x.ghost{ background:transparent; border-color:transparent; } +.btn-x.sm{ padding:5px 10px; font-size:12px; } +.btn-x.danger{ color:var(--danger); border-color:transparent; background:transparent; } +.btn-x.danger:hover{ background:var(--danger-soft); border-color:var(--danger-soft); } + +/* row actions */ +.act-row{ display:flex; align-items:center; gap:3px; justify-content:flex-end; } +.act-btn{ width:30px; height:30px; border-radius:8px; border:1px solid transparent; background:transparent; + color:var(--text-muted); font-size:14px; display:grid; place-items:center; cursor:pointer; transition:background .12s,color .12s; } +.act-btn:hover{ background:var(--panel-hover); color:var(--text); } +.act-btn.go:hover{ color:var(--ok); } .act-btn.stopc:hover{ color:var(--danger); } .act-btn.pri:hover{ color:var(--accent); } + +/* form controls (opt-in .inp helpers from the design system) */ +.field{ margin-bottom:14px; } +.field > label{ display:block; font-size:12px; font-weight:600; color:var(--text-muted); margin-bottom:6px; } +.field .hint{ font-size:11px; color:var(--text-faint); margin-top:5px; } +.inp, select.inp, textarea.inp{ width:100%; background:var(--panel-2); border:1px solid var(--border); border-radius:9px; + color:var(--text); font-size:13.5px; padding:9px 12px; font-family:inherit; transition:border-color .12s,box-shadow .12s; outline:none; } +.inp:focus{ border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-soft); } +.inp::placeholder{ color:var(--text-faint); } +select.inp{ appearance:none; background-image:url("data:image/svg+xml;utf8,"); + background-repeat:no-repeat; background-position:right 11px center; padding-right:32px; } +.search-inp{ position:relative; } +.search-inp .bi{ position:absolute; left:12px; top:50%; transform:translateY(-50%); color:var(--text-faint); font-size:15px; } +.search-inp input{ padding-left:34px; } + +/* toggle switch */ +.sw{ width:38px; height:22px; border-radius:20px; background:var(--panel-2); border:1px solid var(--border); position:relative; cursor:pointer; transition:background .15s; flex-shrink:0; } +.sw::after{ content:""; position:absolute; top:2px; left:2px; width:16px; height:16px; border-radius:50%; background:var(--text-faint); transition:transform .15s,background .15s; } +.sw.on{ background:var(--accent); border-color:var(--accent); } .sw.on::after{ transform:translateX(16px); background:#fff; } + +/* node health strip */ +.node-strip{ display:grid; grid-template-columns:repeat(auto-fill,minmax(228px,1fr)); gap:12px; } +.node-card{ background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); padding:13px 14px; + cursor:pointer; transition:border-color .13s,transform .13s,box-shadow .13s; box-shadow:var(--shadow-sm); } +.node-card:hover{ border-color:var(--accent-border); transform:translateY(-1px); } +.node-card.sel{ border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-soft); } +.node-card .nc-top{ display:flex; align-items:center; gap:9px; margin-bottom:11px; } +.node-card .nc-dot{ width:9px; height:9px; border-radius:50%; flex-shrink:0; } +.nc-dot.online{ background:var(--ok); box-shadow:0 0 0 3px var(--ok-soft); } +.nc-dot.maintenance{ background:var(--warn); box-shadow:0 0 0 3px var(--warn-soft); } +.nc-dot.offline{ background:var(--danger); box-shadow:0 0 0 3px var(--danger-soft); } +.node-card .nc-name{ font-weight:700; font-size:13.5px; line-height:1.1; } +.node-card .nc-role{ font-size:10.5px; color:var(--text-faint); } +.node-card .nc-metric{ display:flex; align-items:center; gap:8px; margin-top:7px; } +.node-card .nc-metric .k{ width:34px; font-size:10px; font-weight:700; letter-spacing:.03em; color:var(--text-faint); text-transform:uppercase; } +.node-card .nc-metric .pct{ width:42px; text-align:right; font-size:11.5px; font-variant-numeric:tabular-nums; color:var(--text-muted); } + +/* stepper */ +.stepper{ display:flex; align-items:center; gap:0; overflow-x:auto; padding-bottom:4px; } +.step{ display:flex; align-items:center; gap:10px; flex:0 0 auto; } +.step .sdot{ width:30px; height:30px; border-radius:50%; display:grid; place-items:center; font-size:13px; font-weight:700; + background:var(--panel-2); border:1px solid var(--border); color:var(--text-faint); flex-shrink:0; } +.step.on .sdot{ background:var(--accent); border-color:var(--accent); color:#fff; } +.step.done .sdot{ background:var(--ok-soft); border-color:var(--ok); color:var(--ok); } +.step .slbl{ font-size:12.5px; font-weight:600; color:var(--text-faint); white-space:nowrap; } +.step.on .slbl, .step.done .slbl{ color:var(--text); } +.step-line{ width:34px; height:2px; background:var(--border); margin:0 6px; flex-shrink:0; } +.step.done + .step-line, .step-line.done{ background:var(--ok); } + +.summary-row{ display:flex; justify-content:space-between; padding:9px 0; border-bottom:1px solid var(--border-soft); font-size:13px; } +.summary-row:last-child{ border-bottom:0; } +.summary-row .k{ color:var(--text-muted); } .summary-row .v{ font-weight:600; } + +.divider{ height:1px; background:var(--border-soft); border:0; margin:0; } +.dot-online{ color:var(--ok); } .dot-offline{ color:var(--danger); } + +.tag{ font-size:10.5px; font-weight:600; padding:2px 8px; border-radius:6px; background:var(--accent-soft); color:var(--accent); } +.scrim{ position:fixed; inset:0; background:var(--scrim); z-index:39; backdrop-filter:blur(2px); } + +.filter-bar{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; } +.filter-bar .search-inp{ flex:1 1 240px; max-width:340px; } +.filter-bar select.inp{ width:auto; min-width:130px; } + +.empty-hint{ padding:44px 20px; text-align:center; color:var(--text-faint); } + +/* detail / config / tabs */ +.kv{ display:flex; justify-content:space-between; gap:14px; padding:8px 0; border-bottom:1px solid var(--border-soft); font-size:12.5px; } +.kv:last-child{ border-bottom:0; } +.kv .k{ color:var(--text-muted); white-space:nowrap; } .kv .v{ font-weight:600; text-align:right; word-break:break-word; } +.metric-hero{ display:flex; gap:12px; flex-wrap:wrap; } +.hero-stat{ flex:1 1 140px; background:var(--panel-2); border:1px solid var(--border-soft); border-radius:var(--radius-sm); padding:12px 14px; } +.hero-stat .k{ font-size:10.5px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; color:var(--text-faint); } +.hero-stat .v{ font-size:20px; font-weight:750; font-variant-numeric:tabular-nums; margin-top:3px; letter-spacing:-.3px; } +.back-link{ display:inline-flex; align-items:center; gap:6px; font-size:12.5px; font-weight:600; color:var(--text-muted); cursor:pointer; } +.back-link:hover{ color:var(--text); } +.cfg-grid{ column-count:2; column-gap:20px; } +@media (max-width:900px){ .cfg-grid{ column-count:1; } } +.cfg-box{ break-inside:avoid; margin-bottom:16px; } +.cfg-box h6{ font-size:11px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:var(--text-faint); margin:0 0 6px; } +.confirm-box{ border:1px solid var(--danger); background:var(--danger-soft); border-radius:var(--radius); padding:16px 18px; } +.tabs{ display:flex; gap:4px; border-bottom:1px solid var(--border); overflow-x:auto; } +.tabs button{ border:0; background:transparent; color:var(--text-muted); font-size:13px; font-weight:600; padding:10px 14px; cursor:pointer; border-bottom:2px solid transparent; white-space:nowrap; } +.tabs button:hover{ color:var(--text); } +.tabs button.on{ color:var(--accent); border-bottom-color:var(--accent); } +.pkg-line{ display:flex; align-items:center; gap:10px; font-size:12.5px; padding:6px 0; } +.pkg-line .mono{ color:var(--text-muted); } + +/* ===================== RESPONSIVE ===================== */ +@media (max-width:1199.98px){ + .app-shell{ grid-template-columns:var(--rail-w) 1fr; } + .app-sidebar .brand-text,.app-sidebar .nav-link-item span.lbl,.app-sidebar .nav-badge, + .app-sidebar .nav-count,.app-sidebar .nav-section-label,.app-sidebar .node-pill .meta{ display:none; } + .app-sidebar .nav-link-item{ justify-content:center; padding:11px 0; } + .app-sidebar .sidebar-brand{ justify-content:center; padding:0; } + .app-sidebar .node-pill{ justify-content:center; padding:9px 0; } + .app-sidebar{ transition:width .18s ease; } + .app-sidebar:hover{ width:var(--sidebar-w); box-shadow:24px 0 48px rgba(0,0,0,.35); } + .app-sidebar:hover .brand-text,.app-sidebar:hover .nav-link-item span.lbl,.app-sidebar:hover .nav-badge, + .app-sidebar:hover .nav-count,.app-sidebar:hover .nav-section-label,.app-sidebar:hover .node-pill .meta{ display:revert; } + .app-sidebar:hover .nav-link-item{ justify-content:flex-start; padding:9px 12px; } + .app-sidebar:hover .sidebar-brand{ justify-content:flex-start; padding:0 20px; } + .app-sidebar:hover .node-pill{ justify-content:flex-start; padding:9px 11px; } +} +@media (max-width:767.98px){ + .app-shell{ grid-template-columns:1fr; } + .app-sidebar{ position:fixed; top:0; left:0; bottom:0; width:var(--sidebar-w); transform:translateX(-100%); transition:transform .22s ease; } + .app-sidebar.open{ transform:translateX(0); box-shadow:24px 0 48px rgba(0,0,0,.45); } + .app-sidebar .brand-text,.app-sidebar .nav-link-item span.lbl,.app-sidebar .nav-badge, + .app-sidebar .nav-count,.app-sidebar .nav-section-label,.app-sidebar .node-pill .meta{ display:revert !important; } + .app-sidebar .nav-link-item{ justify-content:flex-start !important; padding:9px 12px !important; } + .app-sidebar .sidebar-brand{ justify-content:flex-start !important; padding:0 20px !important; } + .app-sidebar .node-pill{ justify-content:flex-start !important; padding:9px 11px !important; } + .app-col{ grid-column:1; } + .hamburger{ display:grid; } + .app-main{ padding:18px 14px 44px; } + .cluster-trigger .cl-txt{ display:none; } + .cluster-trigger .cl-name{ max-width:80px; overflow:hidden; text-overflow:ellipsis; } + .page-heading .crumb{ display:none; } + .util-divider{ display:none; } + .tbl-scroll{ overflow-x:auto; } +} +@media (min-width:768px){ .scrim{ display:none !important; } } + +/* ============================================================ + Bootstrap bridge — make existing Bootstrap markup adopt the new + palette, accent, and rounding without per-page edits. + ============================================================ */ +.card{ border-radius:var(--radius); } +.card-header:first-child{ border-radius:var(--radius) var(--radius) 0 0; } +.btn{ border-radius:var(--radius-sm); } +.form-control, .form-select, .input-group-text{ border-radius:var(--radius-sm); } +.dropdown-menu{ border-radius:var(--radius); box-shadow:var(--shadow); padding:6px; } +.dropdown-item{ border-radius:var(--radius-sm); } +.modal-content{ border-radius:var(--radius); } +.list-group{ border-radius:var(--radius-sm); } + +/* Accent + semantic background bridge (dashboard stat cards, badges, progress) */ +.bg-primary{ background-color:var(--accent) !important; } +.bg-info{ background-color:var(--violet) !important; } +.bg-success{ background-color:var(--ok) !important; } +.bg-danger{ background-color:var(--danger) !important; } +.bg-warning{ background-color:var(--warn) !important; } +.bg-secondary{ background-color:var(--text-faint) !important; } +.text-primary{ color:var(--accent) !important; } +.text-success{ color:var(--ok) !important; } +.text-danger{ color:var(--danger) !important; } +.text-warning{ color:var(--warn) !important; } +.text-info{ color:var(--violet) !important; } + +.btn-primary{ --bs-btn-bg:var(--accent); --bs-btn-border-color:var(--accent); + --bs-btn-hover-bg:var(--accent); --bs-btn-hover-border-color:var(--accent); + --bs-btn-active-bg:var(--accent); --bs-btn-active-border-color:var(--accent); + --bs-btn-disabled-bg:var(--accent); --bs-btn-disabled-border-color:var(--accent); } +.btn-primary:hover{ filter:brightness(1.08); } +.btn-outline-primary{ --bs-btn-color:var(--accent); --bs-btn-border-color:var(--accent-border); + --bs-btn-hover-bg:var(--accent); --bs-btn-hover-border-color:var(--accent); + --bs-btn-active-bg:var(--accent); --bs-btn-active-border-color:var(--accent); } +.progress-bar{ background-color:var(--accent); } +.link-primary, a.text-primary{ color:var(--accent) !important; } +.form-check-input:checked{ background-color:var(--accent); border-color:var(--accent); } +.form-check-input:focus{ border-color:var(--accent-border); box-shadow:0 0 0 0.2rem var(--accent-soft); } +.inp.is-valid{ border-color:var(--ok) !important; } +.inp.is-invalid{ border-color:var(--danger) !important; } + +/* ===================== VNC CONSOLE ===================== */ +.vnc{ border:1px solid var(--border); border-radius:var(--radius); overflow:hidden; background:#05070c; box-shadow:var(--shadow-sm); } +.vnc-bar{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; padding:10px 12px; background:var(--bg-elev); border-bottom:1px solid var(--border); } +.vnc-left{ display:flex; align-items:center; gap:10px; margin-right:auto; } +.vnc-tools{ display:flex; align-items:center; gap:6px; flex-wrap:wrap; } +.vnc-status{ display:inline-flex; align-items:center; gap:7px; font-size:12px; font-weight:600; } +.vnc-status .d{ width:8px; height:8px; border-radius:50%; } +.vnc-status.on .d{ background:var(--ok); box-shadow:0 0 0 3px var(--ok-soft); } .vnc-status.on{ color:var(--ok); } +.vnc-status.off .d{ background:var(--text-faint); } .vnc-status.off{ color:var(--text-faint); } +.vnc-status.busy .d{ background:var(--warn); box-shadow:0 0 0 3px var(--warn-soft); } .vnc-status.busy{ color:var(--warn); } +.vnc-screen{ position:relative; aspect-ratio:16/9; background:#0a0e17; display:grid; place-items:center; overflow:hidden; } +.vnc-screen::after{ content:""; position:absolute; inset:0; pointer-events:none; background:repeating-linear-gradient(0deg,rgba(255,255,255,.025) 0 1px,transparent 1px 3px); } +.vnc-term{ font-family:var(--font-mono); font-size:13px; color:#8ad0a8; line-height:1.7; width:100%; max-width:560px; padding:26px; position:relative; z-index:1; } +.vnc-term .p{ color:#5fb0ff; } .vnc-term .cur{ display:inline-block; width:8px; height:15px; background:#8ad0a8; vertical-align:-2px; animation:blink 1.1s step-end infinite; } +@keyframes blink{ 50%{ opacity:0; } } +.vnc-off{ text-align:center; color:var(--text-faint); position:relative; z-index:1; } .vnc-off .bi{ font-size:34px; display:block; margin-bottom:12px; } +.vnc-foot{ display:flex; align-items:center; gap:16px; flex-wrap:wrap; padding:8px 13px; background:var(--bg-elev); border-top:1px solid var(--border); font-size:11.5px; color:var(--text-faint); font-family:var(--font-mono); } +.vnc-foot .s{ display:inline-flex; align-items:center; gap:6px; } diff --git a/templates/base.html b/templates/base.html index 4498ca7..09b42e3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -5,6 +5,8 @@ {% block title %}Proxmox Manager{% endblock %} + + - +