ui: trend panel + range selector on host repo page
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user