diff --git a/internal/views/layouts/console.gohtml b/internal/views/layouts/console.gohtml new file mode 100644 index 0000000..6201f2a --- /dev/null +++ b/internal/views/layouts/console.gohtml @@ -0,0 +1,5 @@ +{{define "shell"}} +
+ {{template "content" .}} +
+{{end}} diff --git a/internal/views/views.go b/internal/views/views.go index 3ab9b3d..147cf94 100644 --- a/internal/views/views.go +++ b/internal/views/views.go @@ -34,13 +34,17 @@ type ViewData struct { func NewRenderer() (*Renderer, error) { functions := template.FuncMap{ - "icon": icon, - "contains": strings.Contains, - "distroIconClass": distroIconClass, - "uptime": formatUptime, - "safeHTML": func(value string) template.HTML { return template.HTML(value) }, - "nowYear": func() int { return time.Now().Year() }, - "lower": strings.ToLower, + "icon": icon, + "contains": strings.Contains, + "distroIconClass": distroIconClass, + "nodeIconClass": nodeIconClass, + "nodeIconPending": nodeIconPending, + "packageManagerIconClass": packageManagerIconClass, + "packageManagerLabel": packageManagerLabel, + "uptime": formatUptime, + "safeHTML": func(value string) template.HTML { return template.HTML(value) }, + "nowYear": func() int { return time.Now().Year() }, + "lower": strings.ToLower, "splitLines": func(value string) []string { var lines []string for _, line := range strings.Split(value, "\n") { @@ -68,6 +72,8 @@ func (r *Renderer) Render(w http.ResponseWriter, name string, data ViewData) { layout := "layouts/app.gohtml" if data.Shell == "auth" { layout = "layouts/auth.gohtml" + } else if data.Shell == "console" { + layout = "layouts/console.gohtml" } parsed, err := template.New("base").Funcs(r.functions).ParseFS( @@ -137,6 +143,57 @@ func distroIconClass(distro string) string { } } +func nodeIconClass(distro, packageManager string) string { + if className := distroIconClass(distro); className != "ti ti-server-2" { + return className + } + + switch strings.ToLower(strings.TrimSpace(packageManager)) { + case "apt": + return "fa-brands fa-debian" + case "pacman": + return "ti ti-brand-archlinux" + default: + return "ti ti-server-2" + } +} + +func nodeIconPending(distro, packageManager string) bool { + distroValue := strings.ToLower(strings.TrimSpace(distro)) + packageValue := strings.ToLower(strings.TrimSpace(packageManager)) + return packageValue == "" && (distroValue == "" || distroValue == "linux") +} + +func packageManagerIconClass(value string) string { + switch strings.ToLower(strings.TrimSpace(value)) { + case "apt": + return "fa-brands fa-debian" + case "pacman": + return "ti ti-brand-archlinux" + case "dnf", "yum", "zypper", "apk", "nix", "emerge": + return "ti ti-package" + default: + return "ti ti-package" + } +} + +func packageManagerLabel(value string) string { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return "Unknown" + } + switch strings.ToLower(trimmed) { + case "apt": + return "APT" + case "dnf": + return "DNF" + case "yum": + return "YUM" + default: + return strings.ToUpper(trimmed[:1]) + strings.ToLower(trimmed[1:]) + } +} + func icon(name string) template.HTML { icons := map[string]string{ "dashboard": ``, diff --git a/web/static/css/app.css b/web/static/css/app.css index 4a4f30c..a1f94aa 100644 --- a/web/static/css/app.css +++ b/web/static/css/app.css @@ -314,11 +314,24 @@ body[data-bs-theme="light"] .app-sidebar { padding: 0.7rem 0.8rem 0.7rem 0.75rem; min-width: 0; position: relative; + display: grid; + grid-template-rows: auto auto 1fr auto; + align-items: start; +} + +.node-tile-heading { + min-width: 0; } .node-tile-metrics { display: grid; gap: 0.45rem; + align-self: start; +} + +.node-tile-meta { + align-self: end; + margin-top: 0.55rem; } .node-metric { @@ -331,6 +344,8 @@ body[data-bs-theme="light"] .app-sidebar { .node-metric .progress { --bs-progress-height: 0.35rem; + display: flex; + align-items: center; margin: 0; } @@ -340,6 +355,20 @@ body[data-bs-theme="light"] .app-sidebar { font-size: 1rem; } +.node-loading-icon { + animation: node-icon-spin 1s linear infinite; +} + +@keyframes node-icon-spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + .node-tile-main .badge { font-size: 0.65rem; padding: 0.35rem 0.45rem; @@ -386,6 +415,48 @@ body[data-bs-theme="light"] .app-sidebar { justify-self: end; } +.system-summary-item { + display: grid; + grid-template-columns: 3.25rem minmax(0, 1fr); + gap: 0.9rem; + align-items: center; +} + +.system-summary-icon { + width: 3rem; + height: 3rem; + font-size: 1.2rem; +} + +.system-summary-label { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--bs-secondary-color); + margin-bottom: 0.15rem; +} + +.system-summary-value { + font-size: 1rem; + font-weight: 700; + color: var(--bs-emphasis-color); +} + +.system-summary-stats { + display: grid; + gap: 0.75rem; + padding-top: 0.25rem; +} + +.system-summary-stat { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + font-size: 0.92rem; +} + .progress { --bs-progress-height: 0.6rem; background: color-mix(in srgb, black 18%, var(--ma-surface-1)); @@ -472,9 +543,11 @@ body[data-bs-theme="light"] .add-vm-panel { } .auth-toggle-option { - display: grid; - gap: 0.2rem; - padding: 0.85rem 0.95rem; + display: flex; + align-items: center; + justify-content: center; + min-height: 3rem; + padding: 0.6rem 0.9rem; border: 1px solid var(--ma-border); border-radius: 0.95rem; cursor: pointer; @@ -490,11 +563,6 @@ body[data-bs-theme="light"] .auth-toggle-option { color: var(--bs-emphasis-color); } -.auth-toggle-text { - font-size: 0.78rem; - color: var(--bs-secondary-color); -} - .btn-check:checked + .auth-toggle-option { border-color: rgba(var(--color-primary-500), 0.7); background: rgba(var(--color-primary-500), 0.12); @@ -540,7 +608,7 @@ body[data-bs-theme="light"] .option-check { min-height: 420px; max-height: 60vh; overflow: auto; - background: #151920; + background: #0b1220; color: #dbeafe; padding: 0; } @@ -556,19 +624,27 @@ body[data-bs-theme="light"] .option-check { } .console-panel { - min-height: calc(100vh - 12rem); + min-height: 100vh; } .console-panel .console-terminal { - height: calc(100vh - 12rem); + height: 100vh; +} + +.console-fullscreen-shell { + height: 100vh; + width: 100vw; + overflow: hidden; } .console-terminal .xterm { height: 100%; + background: #0b1220; padding: 0.9rem 1rem; } .console-terminal .xterm-viewport { + background: #0b1220; border-radius: 1.25rem; }