From 2d46a9289c5b771445b32d112b139df89a17977e Mon Sep 17 00:00:00 2001 From: GigabiteStudios Date: Sat, 20 Jun 2026 18:08:10 -0500 Subject: [PATCH] feat(ui): refresh shell, auth, and node visuals --- internal/views/layouts/app.gohtml | 34 +- internal/views/layouts/auth.gohtml | 17 +- internal/views/layouts/base.gohtml | 3 +- internal/views/pages/dashboard.gohtml | 4 +- internal/views/pages/login.gohtml | 14 +- internal/views/pages/node.gohtml | 22 +- internal/views/pages/register.gohtml | 16 +- internal/views/views.go | 34 +- web/static/css/app.css | 451 +++++++++++++++++++++++--- web/static/js/app.js | 23 ++ 10 files changed, 497 insertions(+), 121 deletions(-) diff --git a/internal/views/layouts/app.gohtml b/internal/views/layouts/app.gohtml index 372f586..0d724fa 100644 --- a/internal/views/layouts/app.gohtml +++ b/internal/views/layouts/app.gohtml @@ -16,6 +16,12 @@ + {{else if eq .CurrentPath "/uptime"}} +
+ +
{{else if eq .CurrentPath "/groups"}} + -

Create account

+
+ Create account +
{{end}} diff --git a/internal/views/pages/node.gohtml b/internal/views/pages/node.gohtml index a1f82a7..50db0e6 100644 --- a/internal/views/pages/node.gohtml +++ b/internal/views/pages/node.gohtml @@ -17,11 +17,21 @@
-
+
@@ -60,8 +70,8 @@
-
- +
+
Distribution
@@ -69,7 +79,7 @@
-
+
diff --git a/internal/views/pages/register.gohtml b/internal/views/pages/register.gohtml index b775686..5ee649d 100644 --- a/internal/views/pages/register.gohtml +++ b/internal/views/pages/register.gohtml @@ -1,22 +1,24 @@ {{define "content"}} -
-

Create Account

+
+

Create account

{{with .Content}}{{with .Error}}
{{.}}
{{end}}{{end}}
- +
- +
- +
- +
-

Already registered? Sign in

+ {{end}} diff --git a/internal/views/views.go b/internal/views/views.go index 46313e5..be8a2fd 100644 --- a/internal/views/views.go +++ b/internal/views/views.go @@ -38,7 +38,6 @@ func NewRenderer() (*Renderer, error) { "contains": strings.Contains, "distroIconClass": distroIconClass, "nodeIconClass": nodeIconClass, - "nodeIconName": nodeIconName, "nodeIconPending": nodeIconPending, "packageManagerIconClass": packageManagerIconClass, "packageManagerLabel": packageManagerLabel, @@ -134,11 +133,11 @@ func distroIconClass(distro string) string { value := strings.ToLower(strings.TrimSpace(distro)) switch { case strings.Contains(value, "ubuntu"): - return "fa-brands fa-ubuntu" + return "fl-ubuntu" case strings.Contains(value, "debian"): - return "fa-brands fa-debian" + return "fl-debian" case strings.Contains(value, "arch"), strings.Contains(value, "manjaro"), strings.Contains(value, "endeavouros"): - return "ti ti-brand-archlinux" + return "fl-archlinux" default: return "ti ti-server-2" } @@ -151,35 +150,14 @@ func nodeIconClass(distro, packageManager string) string { switch strings.ToLower(strings.TrimSpace(packageManager)) { case "apt": - return "fa-brands fa-debian" + return "fl-debian" case "pacman": - return "ti ti-brand-archlinux" + return "fl-archlinux" default: return "ti ti-server-2" } } -func nodeIconName(distro, packageManager string) string { - value := strings.ToLower(strings.TrimSpace(distro)) - switch { - case strings.Contains(value, "ubuntu"): - return "ubuntu" - case strings.Contains(value, "debian"): - return "debian" - case strings.Contains(value, "arch"), strings.Contains(value, "manjaro"), strings.Contains(value, "endeavouros"): - return "arch" - } - - switch strings.ToLower(strings.TrimSpace(packageManager)) { - case "apt": - return "debian" - case "pacman": - return "arch" - default: - return "server" - } -} - func nodeIconPending(distro, packageManager string) bool { distroValue := strings.ToLower(strings.TrimSpace(distro)) packageValue := strings.ToLower(strings.TrimSpace(packageManager)) @@ -189,7 +167,7 @@ func nodeIconPending(distro, packageManager string) bool { func packageManagerIconClass(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "apt": - return "fa-brands fa-debian" + return "ti ti-brand-debian" case "pacman": return "ti ti-brand-archlinux" case "dnf", "yum", "zypper", "apk", "nix", "emerge": diff --git a/web/static/css/app.css b/web/static/css/app.css index 8243933..3dbdf3c 100644 --- a/web/static/css/app.css +++ b/web/static/css/app.css @@ -24,8 +24,7 @@ } body.theme-dark, -body.theme-light, -body.theme-blue { +body.theme-light { --color-primary-50: 239 246 255; --color-primary-100: 219 234 254; --color-primary-200: 191 219 254; @@ -39,37 +38,6 @@ body.theme-blue { --color-primary-950: 23 37 84; } -body.theme-green { - --color-primary-50: 236 253 245; - --color-primary-100: 209 250 229; - --color-primary-200: 167 243 208; - --color-primary-300: 110 231 183; - --color-primary-400: 52 211 153; - --color-primary-500: 16 185 129; - --color-primary-600: 5 150 105; - --color-primary-700: 4 120 87; - --color-primary-800: 6 95 70; - --color-primary-900: 6 78 59; - --color-primary-950: 2 44 34; -} - -body.theme-red { - --color-primary-50: 254 242 242; - --color-primary-100: 254 226 226; - --color-primary-200: 254 202 202; - --color-primary-300: 252 165 165; - --color-primary-400: 248 113 113; - --color-primary-500: 239 68 68; - --color-primary-600: 220 38 38; - --color-primary-700: 185 28 28; - --color-primary-800: 153 27 27; - --color-primary-900: 127 29 29; - --color-primary-950: 69 10 10; -} - -body.theme-blue, -body.theme-green, -body.theme-red, body.theme-dark, body.theme-light { --bs-primary: rgb(var(--color-primary-600)); @@ -133,12 +101,47 @@ a { .auth-card { background: color-mix(in srgb, var(--ma-surface-overlay) 92%, transparent); backdrop-filter: blur(14px); + border: 1px solid var(--ma-border); } body[data-bs-theme="light"] .auth-card { background: color-mix(in srgb, white 58%, var(--ma-surface-base)); } +.auth-shell { + background: + radial-gradient(circle at top center, rgba(var(--color-primary-500), 0.16), transparent 24%), + linear-gradient(180deg, #181c22 0%, #14181e 100%); +} + +body[data-bs-theme="light"] .auth-shell { + background: + radial-gradient(circle at top center, rgba(var(--color-primary-300), 0.28), transparent 22%), + linear-gradient(180deg, #f4f7fb 0%, #eceff4 100%); +} + +.container-tight { + max-width: 26rem; +} + +.auth-logo { + width: 4rem; + height: 4rem; + object-fit: cover; + border-radius: 1rem; + box-shadow: 0 0.75rem 2rem rgba(0, 0, 0, 0.22); +} + +.auth-card-tabler { + border-radius: 1rem; + box-shadow: 0 1.5rem 3.5rem rgba(15, 23, 42, 0.28); +} + +.auth-input { + min-height: 2.75rem; + border-radius: 0.85rem; +} + .auth-brand { background: radial-gradient(circle at top, rgba(var(--color-primary-600), 0.32), transparent 35%), @@ -199,6 +202,28 @@ body[data-bs-theme="light"] .auth-feature { font-size: 1.4rem; } +.chip-icon-plain { + background: transparent; + box-shadow: none; + color: var(--bs-emphasis-color); +} + +.distro-icon { + color: var(--bs-emphasis-color); +} + +.distro-icon.fl-ubuntu { + color: #e95420; +} + +.distro-icon.fl-debian { + color: #d70a53; +} + +.distro-icon.fl-archlinux { + color: #1793d1; +} + .content { min-width: 0; min-height: 0; @@ -228,12 +253,6 @@ body[data-bs-theme="light"] .app-sidebar { background: color-mix(in srgb, white 22%, var(--ma-surface-sidebar)); } -.app-sidebar-nav .btn { - justify-content: flex-start; - align-items: center; - gap: 0.4rem; -} - .sidebar-logo { width: 3rem; height: 3rem; @@ -242,6 +261,75 @@ body[data-bs-theme="light"] .app-sidebar { box-shadow: 0 0.75rem 2rem rgba(var(--color-primary-700), 0.18); } +.app-sidebar-inner { + gap: 0.9rem; +} + +.sidebar-brand { + padding: 0.35rem 0.2rem 0.55rem; +} + +.sidebar-brand-title { + font-size: 1rem; + font-weight: 700; + letter-spacing: -0.02em; +} + +.sidebar-section-label { + padding: 0 0.65rem; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--bs-secondary-color); +} + +.app-sidebar-nav { + gap: 0.35rem; +} + +.app-nav-link { + display: flex; + align-items: center; + gap: 0.8rem; + min-height: 2.9rem; + padding: 0.75rem 0.85rem; + border-radius: 0.95rem; + color: var(--bs-secondary-color); + font-weight: 600; + transition: background-color 0.18s ease, color 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease; +} + +.app-nav-link i { + width: 1.1rem; + font-size: 1.05rem; + text-align: center; +} + +.app-nav-link:hover { + color: var(--bs-emphasis-color); + background: color-mix(in srgb, var(--ma-surface-2) 92%, transparent); + transform: translateX(2px); +} + +.app-nav-link.is-active { + color: #fff; + background: linear-gradient(135deg, rgba(var(--color-primary-600), 0.96), rgba(var(--color-primary-700), 0.96)); + box-shadow: 0 0.9rem 2rem rgba(var(--color-primary-900), 0.2); +} + +body[data-bs-theme="light"] .app-nav-link.is-active { + color: #fff; +} + +.app-header { + box-shadow: inset 0 -1px 0 var(--ma-border); +} + +.app-footer { + box-shadow: inset 0 1px 0 var(--ma-border); +} + .min-w-0 { min-width: 0; } @@ -285,6 +373,7 @@ body[data-bs-theme="light"] .app-sidebar { align-items: center; justify-content: center; padding: 0; + border-radius: 0.85rem; } .node-chip { @@ -311,6 +400,90 @@ body[data-bs-theme="light"] .app-sidebar { position: relative; } +.kpi-card-featured .card-body { + min-height: 12rem; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.kpi-card-label { + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--bs-secondary-color); +} + +.kpi-card-subtle { + margin-top: 0.35rem; + font-size: 0.9rem; + color: var(--bs-secondary-color); +} + +.kpi-card-chip { + display: inline-flex; + align-items: center; + min-height: 2rem; + padding: 0.35rem 0.7rem; + border-radius: 999px; + border: 1px solid var(--ma-border); + background: color-mix(in srgb, var(--ma-surface-base) 78%, var(--ma-surface-2)); + color: var(--bs-secondary-color); + font-size: 0.76rem; + font-weight: 600; + white-space: nowrap; +} + +.kpi-card-featured-value { + position: relative; + z-index: 1; + font-size: clamp(2.3rem, 4vw, 3.15rem); + font-weight: 800; + line-height: 1; + letter-spacing: -0.03em; + color: var(--bs-emphasis-color); +} + +.kpi-card-featured-trend { + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 0.7rem; + flex-wrap: wrap; +} + +.kpi-card-trend { + display: inline-flex; + align-items: center; + min-height: 2rem; + padding: 0.3rem 0.65rem; + border-radius: 999px; + font-size: 0.82rem; + font-weight: 700; +} + +.kpi-card-trend.is-up { + color: #86efac; + background: rgba(20, 83, 45, 0.58); +} + +.kpi-card-trend.is-down { + color: #fca5a5; + background: rgba(127, 29, 29, 0.58); +} + +.kpi-card-trend.is-flat { + color: #cbd5e1; + background: rgba(51, 65, 85, 0.6); +} + +.kpi-card-trend-copy { + font-size: 0.88rem; + color: var(--bs-secondary-color); +} + .kpi-graph { position: absolute; inset: 0; @@ -323,6 +496,13 @@ body[data-bs-theme="light"] .app-sidebar { height: 100%; } +.kpi-card-featured .kpi-graph { + inset: auto 0 0 0; + height: 68%; + opacity: 0.92; + mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.45) 26%, rgba(0, 0, 0, 1) 100%); +} + .node-tile { display: grid; grid-template-columns: 64px minmax(0, 1fr); @@ -382,6 +562,12 @@ body[data-bs-theme="light"] .app-sidebar { font-size: 1rem; } +.node-brand-icon { + width: 3rem; + height: 3rem; + font-size: 2.1rem; +} + .node-loading-icon { animation: node-icon-spin 1s linear infinite; } @@ -455,6 +641,16 @@ body[data-bs-theme="light"] .app-sidebar { font-size: 1.2rem; } +.system-summary-icon-plain { + width: 3.5rem; + height: 3.5rem; + font-size: 2.25rem; +} + +.distro-icon-lg { + font-size: 2.4rem; +} + .system-summary-label { font-size: 0.72rem; font-weight: 700; @@ -515,6 +711,23 @@ body[data-bs-theme="light"] .app-sidebar { background: var(--ma-surface-1); } +.form-control, +.form-select, +.btn, +.modal-content { + border-radius: 0.9rem; +} + +.form-control, +.form-select { + background: color-mix(in srgb, var(--ma-surface-base) 80%, var(--ma-surface-2)); +} + +body[data-bs-theme="light"] .form-control, +body[data-bs-theme="light"] .form-select { + background: color-mix(in srgb, white 72%, var(--ma-surface-1)); +} + .stat-card, .node-chip, .dashboard-loader > .card, @@ -739,23 +952,159 @@ body[data-bs-theme="light"] .theme-card { background: linear-gradient(135deg, #ffffff, #e2e8f0); } -.swatch-green { - background: linear-gradient(135deg, rgb(16 185 129), rgb(4 120 87)); -} - -.swatch-red { - background: linear-gradient(135deg, rgb(239 68 68), rgb(185 28 28)); -} - -.swatch-blue { - background: linear-gradient(135deg, rgb(59 130 246), rgb(29 78 216)); -} - .btn-check:checked + .theme-card { border-color: rgb(var(--color-primary-500)); box-shadow: 0 0 0 0.2rem rgba(var(--color-primary-500), 0.2); } +.uptime-summary-card { + background: linear-gradient(180deg, color-mix(in srgb, var(--ma-surface-2) 92%, transparent), var(--ma-surface-1)); +} + +.uptime-summary-label { + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--bs-secondary-color); + margin-bottom: 0.6rem; +} + +.uptime-summary-value { + font-size: clamp(1.8rem, 3vw, 2.5rem); + font-weight: 800; + line-height: 1; + color: var(--bs-emphasis-color); +} + +.uptime-monitor-card { + display: grid; + gap: 1rem; + height: 100%; + padding: 1.1rem; + border: 1px solid var(--ma-border); + border-radius: 1.15rem; + background: linear-gradient(180deg, color-mix(in srgb, var(--ma-surface-2) 90%, transparent), var(--ma-surface-1)); +} + +.uptime-monitor-card.is-up { + box-shadow: inset 0 0 0 1px rgba(34, 197, 94, 0.12); +} + +.uptime-monitor-card.is-down { + box-shadow: inset 0 0 0 1px rgba(239, 68, 68, 0.14); +} + +.uptime-monitor-head, +.uptime-monitor-foot, +.uptime-monitor-stat { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.uptime-monitor-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 4.5rem; + min-height: 2rem; + padding: 0.35rem 0.75rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.uptime-monitor-badge.is-up { + color: #86efac; + background: rgba(20, 83, 45, 0.55); +} + +.uptime-monitor-badge.is-down { + color: #fca5a5; + background: rgba(127, 29, 29, 0.55); +} + +.uptime-monitor-badge.is-pending { + color: #cbd5e1; + background: rgba(51, 65, 85, 0.65); +} + +.uptime-monitor-stats { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.8rem 1rem; +} + +.uptime-monitor-stat { + padding: 0.75rem 0.85rem; + border-radius: 0.9rem; + background: color-mix(in srgb, var(--ma-surface-base) 74%, var(--ma-surface-2)); + font-size: 0.86rem; +} + +.uptime-monitor-stat span, +.uptime-monitor-foot { + color: var(--bs-secondary-color); +} + +.uptime-monitor-stat strong { + color: var(--bs-emphasis-color); + font-weight: 700; +} + +.uptime-check-strip { + display: flex; + align-items: flex-end; + gap: 0.28rem; + min-height: 2.25rem; + padding: 0.2rem 0; +} + +.uptime-check-pill { + flex: 1 1 0; + min-width: 0; + height: 2rem; + border-radius: 999px; + background: rgba(100, 116, 139, 0.35); +} + +.uptime-check-pill.is-up { + background: linear-gradient(180deg, rgba(34, 197, 94, 0.95), rgba(22, 163, 74, 0.45)); +} + +.uptime-check-pill.is-down { + background: linear-gradient(180deg, rgba(248, 113, 113, 0.95), rgba(185, 28, 28, 0.5)); +} + +.uptime-check-pill.is-pending { + background: rgba(100, 116, 139, 0.35); +} + +.uptime-monitor-foot { + font-size: 0.82rem; +} + +.uptime-table th { + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--bs-secondary-color); +} + +body[data-bs-theme="light"] .uptime-monitor-card, +body[data-bs-theme="light"] .uptime-summary-card { + background: linear-gradient(180deg, color-mix(in srgb, white 76%, var(--ma-surface-1)), var(--ma-surface-1)); +} + +body[data-bs-theme="light"] .uptime-monitor-stat { + background: color-mix(in srgb, white 82%, var(--ma-surface-1)); +} + @media (max-width: 991.98px) { .app-shell { flex-direction: column; diff --git a/web/static/js/app.js b/web/static/js/app.js index 0ca5ee6..dbcbb74 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -8,6 +8,8 @@ document.addEventListener("DOMContentLoaded", () => { const dashboardNodes = document.getElementById("dashboard-nodes"); const dashboardSearch = document.getElementById("dashboard-search"); const authToggle = document.querySelector("[data-auth-toggle]"); + const themeRadios = document.querySelectorAll('input[name="theme"]'); + const themeModeInput = document.querySelector('input[name="mode"]'); const attachXtermConsole = (consoleOutput) => { const wsPath = consoleOutput.dataset.ws; @@ -143,6 +145,7 @@ document.addEventListener("DOMContentLoaded", () => { disk: [], uptime: [], }; + let previousCpu = null; const uptimeLabel = (seconds) => { const days = Math.floor(seconds / 86400); @@ -200,11 +203,21 @@ document.addEventListener("DOMContentLoaded", () => { const ramValue = nodeLive.querySelector('[data-kpi-value="ram"]'); const diskValue = nodeLive.querySelector('[data-kpi-value="disk"]'); const uptimeValue = nodeLive.querySelector('[data-kpi-value="uptime"]'); + const cpuTrend = nodeLive.querySelector('[data-kpi-trend="cpu"]'); if (cpuValue) cpuValue.textContent = `${cpu.toFixed(1)}%`; if (ramValue) ramValue.textContent = `${ram.toFixed(1)}%`; if (diskValue) diskValue.textContent = `${disk.toFixed(1)}%`; if (uptimeValue) uptimeValue.textContent = uptimeLabel(uptime); + if (cpuTrend) { + const delta = previousCpu === null ? 0 : cpu - previousCpu; + const trendClass = delta > 0.15 ? "is-up" : delta < -0.15 ? "is-down" : "is-flat"; + const trendLabel = `${delta > 0 ? "+" : ""}${delta.toFixed(1)}%`; + cpuTrend.classList.remove("is-up", "is-down", "is-flat"); + cpuTrend.classList.add(trendClass); + cpuTrend.textContent = trendLabel; + } + previousCpu = cpu; pushValue("cpu", cpu); pushValue("ram", ram); @@ -294,6 +307,16 @@ document.addEventListener("DOMContentLoaded", () => { syncAuthMode(); } + if (themeModeInput instanceof HTMLInputElement) { + themeRadios.forEach((radio) => { + radio.addEventListener("change", () => { + if (radio instanceof HTMLInputElement && radio.checked) { + themeModeInput.value = radio.value; + } + }); + }); + } + if (typeof bootstrap !== "undefined") { document.querySelectorAll('[data-bs-toggle-tooltip="tooltip"]').forEach((element) => { new bootstrap.Tooltip(element);