mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
webui: new page, services table
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/kernelkit/webui/internal/restconf"
|
||||
)
|
||||
|
||||
// ─── RESTCONF JSON types ──────────────────────────────────────────────────────
|
||||
|
||||
type servicesWrapper struct {
|
||||
SystemState struct {
|
||||
Services struct {
|
||||
Service []serviceJSON `json:"service"`
|
||||
} `json:"infix-system:services"`
|
||||
} `json:"ietf-system:system-state"`
|
||||
}
|
||||
|
||||
type serviceJSON struct {
|
||||
Name string `json:"name"`
|
||||
PID uint32 `json:"pid"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Statistics serviceStatsJSON `json:"statistics"`
|
||||
}
|
||||
|
||||
// memory-usage and uptime are marshalled as strings by the statd Python layer.
|
||||
type serviceStatsJSON struct {
|
||||
MemoryUsage json.Number `json:"memory-usage"`
|
||||
Uptime json.Number `json:"uptime"`
|
||||
RestartCount uint32 `json:"restart-count"`
|
||||
}
|
||||
|
||||
func (s serviceStatsJSON) memoryBytes() uint64 {
|
||||
v, _ := strconv.ParseUint(s.MemoryUsage.String(), 10, 64)
|
||||
return v
|
||||
}
|
||||
|
||||
func (s serviceStatsJSON) uptimeSeconds() uint64 {
|
||||
v, _ := strconv.ParseUint(s.Uptime.String(), 10, 64)
|
||||
return v
|
||||
}
|
||||
|
||||
// ─── Template data ────────────────────────────────────────────────────────────
|
||||
|
||||
type servicesPageData struct {
|
||||
PageData
|
||||
Services []serviceEntry
|
||||
Error string
|
||||
}
|
||||
|
||||
type serviceEntry struct {
|
||||
Name string
|
||||
Status string
|
||||
StatusClass string // "svc-running", "svc-stopped", "svc-error", "svc-done"
|
||||
PID string
|
||||
Memory string
|
||||
Uptime string
|
||||
RestartCount uint32
|
||||
Description string
|
||||
}
|
||||
|
||||
// ─── Handler ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// ServicesHandler serves the system services page.
|
||||
type ServicesHandler struct {
|
||||
Template *template.Template
|
||||
RC *restconf.Client
|
||||
}
|
||||
|
||||
// Overview renders the services page (GET /services).
|
||||
func (h *ServicesHandler) Overview(w http.ResponseWriter, r *http.Request) {
|
||||
data := servicesPageData{
|
||||
PageData: newPageData(r, "services", "Services"),
|
||||
}
|
||||
|
||||
var raw servicesWrapper
|
||||
if err := h.RC.Get(r.Context(), "/data/ietf-system:system-state/infix-system:services", &raw); err != nil {
|
||||
log.Printf("restconf services: %v", err)
|
||||
data.Error = "Could not fetch services data"
|
||||
} else {
|
||||
for _, svc := range raw.SystemState.Services.Service {
|
||||
data.Services = append(data.Services, buildServiceEntry(svc))
|
||||
}
|
||||
sort.Slice(data.Services, func(i, j int) bool {
|
||||
return data.Services[i].Name < data.Services[j].Name
|
||||
})
|
||||
}
|
||||
|
||||
tmplName := "services.html"
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
tmplName = "content"
|
||||
}
|
||||
if err := h.Template.ExecuteTemplate(w, tmplName, data); err != nil {
|
||||
log.Printf("template error: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func buildServiceEntry(svc serviceJSON) serviceEntry {
|
||||
pid := ""
|
||||
if svc.PID > 0 {
|
||||
pid = fmt.Sprintf("%d", svc.PID)
|
||||
}
|
||||
|
||||
return serviceEntry{
|
||||
Name: svc.Name,
|
||||
Status: svc.Status,
|
||||
StatusClass: serviceStatusClass(svc.Status),
|
||||
PID: pid,
|
||||
Memory: formatServiceMemory(svc.Statistics.memoryBytes()),
|
||||
Uptime: formatServiceUptime(svc.Statistics.uptimeSeconds()),
|
||||
RestartCount: svc.Statistics.RestartCount,
|
||||
Description: svc.Description,
|
||||
}
|
||||
}
|
||||
|
||||
func serviceStatusClass(status string) string {
|
||||
switch status {
|
||||
case "running", "active", "done":
|
||||
return "svc-running" // green
|
||||
case "crashed", "failed", "halted", "missing", "dead", "conflict":
|
||||
return "svc-error" // red
|
||||
default:
|
||||
// stopped, paused, and anything else → yellow
|
||||
return "svc-stopped"
|
||||
}
|
||||
}
|
||||
|
||||
// formatServiceMemory mirrors the CLI's format_memory_bytes.
|
||||
func formatServiceMemory(b uint64) string {
|
||||
switch {
|
||||
case b == 0:
|
||||
return ""
|
||||
case b < 1024:
|
||||
return fmt.Sprintf("%dB", b)
|
||||
case b < 1024*1024:
|
||||
return fmt.Sprintf("%dK", b/1024)
|
||||
case b < 1024*1024*1024:
|
||||
return fmt.Sprintf("%.1fM", float64(b)/float64(1024*1024))
|
||||
default:
|
||||
return fmt.Sprintf("%.1fG", float64(b)/float64(1024*1024*1024))
|
||||
}
|
||||
}
|
||||
|
||||
// formatServiceUptime mirrors the CLI's format_uptime_seconds.
|
||||
func formatServiceUptime(s uint64) string {
|
||||
switch {
|
||||
case s == 0:
|
||||
return ""
|
||||
case s < 60:
|
||||
return fmt.Sprintf("%ds", s)
|
||||
case s < 3600:
|
||||
return fmt.Sprintf("%dm", s/60)
|
||||
case s < 86400:
|
||||
return fmt.Sprintf("%dh", s/3600)
|
||||
default:
|
||||
return fmt.Sprintf("%dd", s/86400)
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,10 @@ func New(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servicesTmpl, err := template.ParseFS(templateFS, "layouts/*.html", "pages/services.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containersTmpl, err := template.ParseFS(templateFS, "layouts/*.html", "pages/containers.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -126,6 +130,7 @@ func New(
|
||||
ntp := &handlers.NTPHandler{Template: ntpTmpl, RC: rc}
|
||||
lldp := &handlers.LLDPHandler{Template: lldpTmpl, RC: rc}
|
||||
mdns := &handlers.MDNSHandler{Template: mdnsTmpl, RC: rc}
|
||||
services := &handlers.ServicesHandler{Template: servicesTmpl, RC: rc}
|
||||
containers := &handlers.ContainersHandler{Template: containersTmpl, RC: rc}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
@@ -159,6 +164,7 @@ func New(
|
||||
mux.HandleFunc("GET /ntp", ntp.Overview)
|
||||
mux.HandleFunc("GET /lldp", lldp.Overview)
|
||||
mux.HandleFunc("GET /mdns", mdns.Overview)
|
||||
mux.HandleFunc("GET /services", services.Overview)
|
||||
mux.HandleFunc("GET /containers", containers.Overview)
|
||||
|
||||
handler := authMiddleware(store, mux)
|
||||
|
||||
@@ -1136,6 +1136,31 @@ details[open].nav-group-top > summary.nav-group-summary-top::after {
|
||||
|
||||
.mdns-lastseen { white-space: nowrap; }
|
||||
|
||||
/* Services table */
|
||||
.svc-name { font-family: var(--font-mono); font-size: 0.85rem; white-space: nowrap; }
|
||||
.svc-num { white-space: nowrap; color: var(--fg-muted); font-size: 0.85rem; }
|
||||
.svc-desc { color: var(--fg-muted); font-size: 0.875rem; }
|
||||
.svc-status {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: lowercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.svc-running { color: #16a34a; }
|
||||
.svc-stopped { color: #ca8a04; }
|
||||
.svc-error { color: #dc2626; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.svc-running { color: #86efac; }
|
||||
.svc-stopped { color: #fde047; }
|
||||
.svc-error { color: #fca5a5; }
|
||||
}
|
||||
.dark .svc-running { color: #86efac; }
|
||||
.dark .svc-stopped { color: #fde047; }
|
||||
.dark .svc-error { color: #fca5a5; }
|
||||
.light .svc-running { color: #16a34a; }
|
||||
.light .svc-stopped { color: #ca8a04; }
|
||||
.light .svc-error { color: #dc2626; }
|
||||
|
||||
/* Sub-heading within a card body (e.g. "Active Leases" in DHCP) */
|
||||
.section-subtitle {
|
||||
font-size: 0.75rem;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="20" height="4" rx="1"/>
|
||||
<rect x="2" y="10" width="20" height="4" rx="1"/>
|
||||
<rect x="2" y="17" width="20" height="4" rx="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 344 B |
@@ -122,6 +122,16 @@
|
||||
|
||||
<div class="nav-section-label">Services</div>
|
||||
<ul class="nav-group-items">
|
||||
<li>
|
||||
<a href="/services"
|
||||
hx-get="/services"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "services"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/services.svg')"></span>
|
||||
Services
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/dhcp"
|
||||
hx-get="/dhcp"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{{define "services.html"}}
|
||||
{{template "base" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
{{if .Error}}
|
||||
<div class="alert alert-error">{{.Error}}</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Services}}
|
||||
<section class="info-card">
|
||||
<div class="card-header">Services</div>
|
||||
<div class="data-table-wrap">
|
||||
<table class="data-table svc-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th class="num">PID</th>
|
||||
<th class="num">Mem</th>
|
||||
<th class="num">Up</th>
|
||||
<th class="num">Rst</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Services}}
|
||||
<tr>
|
||||
<td class="svc-name">{{.Name}}</td>
|
||||
<td><span class="svc-status {{.StatusClass}}">{{.Status}}</span></td>
|
||||
<td class="num svc-num">{{.PID}}</td>
|
||||
<td class="num svc-num">{{.Memory}}</td>
|
||||
<td class="num svc-num">{{.Uptime}}</td>
|
||||
<td class="num svc-num">{{if .RestartCount}}{{.RestartCount}}{{end}}</td>
|
||||
<td class="svc-desc">{{.Description}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{{else if not .Error}}
|
||||
<section class="info-card">
|
||||
<div class="card-header">Services</div>
|
||||
<p class="empty-message">No services found.</p>
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user