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;
}