Files
felhom.eu/hub/internal/web/offsite_box_render_test.go
T
admin 4bb2df0dc4 hub v0.64.0 — offsite pool-box aggregate: fill, oversubscription, per-customer bars, operator alert (R-5)
The operator sees the shared pool box's real state on the hub: total box fill vs
capacity, Σ(shared soft quotas) vs capacity (the oversubscription ratio), per-customer
usage/quota bars, and a box-level operator alert (fill % + oversub ratio) on the existing
dispatcher's operator channel. Per-customer fill alerts already existed; the box-level
aggregate was the gap. READ-ONLY against Hetzner (GET only).

Phase-0 probe (gate PASSED): the live pool box 611714 returns capacity via
storage_box_type.size (1 TiB / bx11) and usage via a stats object (size/size_data/
size_snapshots), all bytes; our token reads it (200).

- hetznerapi: additive StorageBoxType + StorageBoxStats on StorageBox (no existing field/
  method changed); fake carries them + a GetBoxCalls counter; golden decode test.
- monitor.OffsiteBoxChecker: OffsiteChecker-sibling for the box; fetch-throttled (1 GET/
  15min), cached BoxSnapshot, escalation-only + recovery re-arm. FILL (used/capacity 80/90)
  + OVERSUB (Σ shared+enabled quotas / capacity, 2.0x) — independent. Σ from the ConfigJSON
  Descriptor (offsite.ReadDescriptor, new), never the report echo; dedicated+disabled
  excluded. Scope "pool-box" -> operator channel only, no SaveEvent. Failed fetch keeps the
  last snapshot degraded; missing data never becomes 0% and never transitions a band.
- config: Alerting.OffsiteBoxFill{Warn,Crit}Percent + OffsiteOversubWarnRatio (80/90/2.0
  defaults; thresholds pending Viktor's ruling). Constructed in the HETZNER_TOKEN branch,
  60s sweep, snapshot handed to the web server.
- web: Offsite-tab panel (fill bar, Σ+ratio, per-customer usage/quota rows) + a compact
  dashboard tile; reads the cached snapshot only, never fetches; nil -> "not configured".

Tests: 10 new + 4 red-proofs (throttle, Σ filter, escalation-only, failed-fetch honesty),
all confirmed red then restored. go build/vet/test all pass; hub confirm gate OK.
2026-07-17 20:15:34 +02:00

86 lines
2.7 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)
}
}
if strings.Contains(body, "not configured") {
t.Fatal("a configured 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:])
}