ui: trend panel + range selector on host repo page

This commit is contained in:
2026-05-07 19:10:59 +01:00
parent a48df77f40
commit 8be551349c
7 changed files with 204 additions and 0 deletions
+60
View File
@@ -1,9 +1,12 @@
package http
import (
"context"
"encoding/json"
"errors"
"html/template"
"log/slog"
"math"
stdhttp "net/http"
"strconv"
"strings"
@@ -13,6 +16,7 @@ import (
"gitea.dcglab.co.uk/steve/restic-manager/internal/server/ui"
"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
"gitea.dcglab.co.uk/steve/restic-manager/internal/web/sparkline"
)
// ui_repo.go — HTML form-driven repo-tab handlers (connection,
@@ -27,6 +31,15 @@ import (
// POST /hosts/{id}/admin-credentials — admin (prune) creds
// POST /hosts/{id}/admin-credentials/delete — clear admin creds
// repoTrendView is the data the repo_size_chart partial needs.
// HostID + Range round-trip through the htmx range pills; ChartSVG
// is pre-rendered server-side so the partial is just a wrapper.
type repoTrendView struct {
HostID string
Range string
ChartSVG template.HTML
}
// repoStatsView is a flat, pre-dereferenced projection of
// store.HostRepoStats for use in templates. Nil pointer fields are
// collapsed to zero/false and accompanied by a Has* sentinel so the
@@ -74,6 +87,10 @@ type hostRepoPage struct {
// Nil when no row exists yet (fresh hosts).
StatsView *repoStatsView
// Trend holds the pre-rendered chart fragment data for the
// 30/90/365-day repo-size + snapshot-count overlay chart.
Trend repoTrendView
// Snapshots-by-tag — map[group_name]count, plus an "untagged" row.
SnapshotsByTag map[string]int
UntaggedSnapshots int
@@ -225,9 +242,52 @@ func (s *Server) loadHostRepoPage(r *stdhttp.Request, host store.Host) (*hostRep
}
}
}
p.Trend = s.buildRepoTrendView(r.Context(), host.ID, "30d")
return p, nil
}
// buildRepoTrendView builds the chart-partial data for a host. Used
// both by the page-load (initial 30d render) and the htmx fragment
// endpoint (range switching). An invalid rangeKey falls back to "30d".
func (s *Server) buildRepoTrendView(ctx context.Context, hostID, rangeKey string) repoTrendView {
days := 30
switch rangeKey {
case "90d":
days = 90
case "1y":
days = 365
default:
rangeKey = "30d"
}
since := time.Now().UTC().AddDate(0, 0, -days)
pts, err := s.deps.Store.ListHostRepoStatsHistory(ctx, hostID, since)
if err != nil {
slog.Warn("ui repo trend: list history", "host_id", hostID, "err", err)
}
sizes := make([]float64, len(pts))
counts := make([]float64, len(pts))
dayList := make([]time.Time, len(pts))
for i, p := range pts {
dayList[i] = p.Day
if p.TotalSizeBytes == nil {
sizes[i] = math.NaN()
} else {
sizes[i] = float64(*p.TotalSizeBytes)
}
if p.SnapshotCount == nil {
counts[i] = math.NaN()
} else {
counts[i] = float64(*p.SnapshotCount)
}
}
chartSVG := sparkline.RenderChart([]sparkline.Series{
{Name: "size", Stroke: "#3b82f6", Axis: sparkline.AxisLeft, Format: sparkline.FormatBytes, Points: sizes},
{Name: "snapshots", Stroke: "#f59e0b", Axis: sparkline.AxisRight, Format: sparkline.FormatCount, Points: counts},
}, dayList, sparkline.ChartOpts{Width: 600, Height: 220})
return repoTrendView{HostID: hostID, Range: rangeKey, ChartSVG: chartSVG}
}
func (s *Server) handleUIHostRepo(w stdhttp.ResponseWriter, r *stdhttp.Request) {
u := s.requireUIUser(w, r)
if u == nil {