Files
felhom.eu/hub/internal/web/offsite_box_render_test.go
T
admin 7f11cfb36c hub v0.65.0 — PBS DR storage visibility (ep0 usage op) + Offsite tab split + dual dashboard gauges (R-5)
Makes PBS DR storage visible like the restic pool box (v0.64.0), differentiated. Scoping
correction: restic = subaccounts on the shared Hetzner Storage Box (Hetzner API); PBS DR =
the felhom-offsite PBS datastore on the ep0 endpoint VM (NO Hetzner API). Option A
(Viktor-ruled): a read-only `usage` op on the felhom-tenantsync ep0 forced command (twin of
fingerprint), polled by a new hub checker on the 15-min throttle. READ-ONLY throughout.

Phase-0 (gate PASSED): on ep0 (PBS 4.2.3), df -B1 --output=size,used,avail <datastore path>
yields bytes (39990112256/7627939840/... ~19%), read-only, existing sudo context, no admin token.

- scripts/felhom-tenantsync.sh -> v1.2.0: read-only `usage` short-circuit (df on the datastore
  path), no customer_id, no admin token, NO mutation. + a bash harness proving zero mutation.
- tenantsync.Client.Usage() + BoxUsage; unknown-op -> typed ErrUsageUnsupported (graceful).
- monitor.PBSDRBoxChecker: OffsiteBoxChecker clone over a usageReader seam; 15-min throttle,
  cached PBSBoxSnapshot, escalation-only pbsdr_box_fill on the "pbsdr-box" scope (operator only,
  no SaveEvent), recovery re-arm. Fill only. THREE states: ok / unavailable (ep0 <=v1.1.0,
  neutral no-alert) / degraded (exec failed, keep last).
- config: Alerting.PBSDRBoxFill{Warn,Crit}Percent (80/90); built with the tenantsync client,
  60s sweep, SetPBSDRBox. Hub deploy INDEPENDENT of the ep0 update (graceful degradation).
- web: /offsite splits into Restic + PBS DR hash tabs (endpoint cards under PBS DR); PBS panel;
  the single dashboard tile becomes two gauges (RESTIC pct.ratio, PBS DR pct / n/a).
- runbook offsite-endpoint.md 10: v1.2.0 update steps (no sudoers/authorized_keys change).

Tests: 10 Go + the harness; 3 red-proofs (usage mutation, escalation-only, unavailable-drives-band)
confirmed red then restored. go build/vet/test + bash -n + hub confirm gate all pass.
2026-07-17 21:13:30 +02:00

88 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package web
import (
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/monitor"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
const gib = int64(1) << 30
// Not configured (no HETZNER_TOKEN) → honest "not configured", never an error or fake zeros.
// (Uses renderOffsite from offsite_test.go — the real handler via httptest.)
func TestOffsiteBoxPanel_NotConfigured(t *testing.T) {
s, _ := newRenderServer(t)
// s.offsiteBox left nil
body := renderOffsite(t, s)
if !strings.Contains(body, "Offsite pool metrics not configured") {
t.Fatalf("unconfigured panel must say 'not configured':\n%s", body)
}
if strings.Contains(body, "0.0 GB") {
t.Fatal("unconfigured panel must not render fake zeros")
}
}
// With data + per-customer rows: the panel renders capacity/fill/ratio; a shared customer with a report
// shows usage, a customer with NO report shows 'no usage reported yet' (never an asserted 0 GB).
func TestOffsiteBoxPanel_WithData(t *testing.T) {
s, st := newRenderServer(t)
s.SetOffsiteBox(func() (monitor.BoxSnapshot, bool) {
return monitor.BoxSnapshot{
CapacityBytes: 1 << 40, UsedBytes: 300 * gib, DataBytes: 300 * gib,
FillPercent: 27, FillBand: "ok", SumQuotaGB: 1200, Ratio: 1.17, OversubBand: "ok",
BoxType: "bx11", FetchedAt: time.Now().UTC(),
}, true
})
// "acme": shared, enabled, 500 GB, WITH a report (200 GiB used). "newbie": shared, enabled, no report.
saveCfg(t, st, "acme", `{"offsite":{"enabled":true,"type":"shared","quota_gb":500}}`)
saveCfg(t, st, "newbie", `{"offsite":{"enabled":true,"type":"shared","quota_gb":300}}`)
if err := st.SaveReport("acme", []byte(`{"customer_id":"acme","offsite":{"enabled":true,"repo_size_bytes":`+itoa(200*gib)+`}}`)); err != nil {
t.Fatal(err)
}
body := renderOffsite(t, s)
for _, want := range []string{"Offsite pool box", "1.00 TB", "1.17×", "acme", "newbie", "no usage reported yet"} {
if !strings.Contains(body, want) {
t.Fatalf("panel missing %q:\n%s", want, body)
}
}
// The RESTIC panel is configured — its specific not-configured message must be absent. (The PBS DR
// panel legitimately shows its own "not configured" here since s.pbsdrBox is unset.)
if strings.Contains(body, "Offsite pool metrics not configured") {
t.Fatal("a configured restic panel must not show the not-configured message")
}
}
func saveCfg(t *testing.T, st *store.Store, id, cfgJSON string) {
t.Helper()
if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: id, CustomerName: id, APIKey: "k-" + id, RetrievalPassword: "p", ConfigJSON: cfgJSON}); err != nil {
t.Fatalf("save config %s: %v", id, err)
}
}
// itoa avoids importing strconv just for the report body.
func itoa(v int64) string {
if v == 0 {
return "0"
}
neg := v < 0
if neg {
v = -v
}
var b [20]byte
i := len(b)
for v > 0 {
i--
b[i] = byte('0' + v%10)
v /= 10
}
if neg {
i--
b[i] = '-'
}
return string(b[i:])
}