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.
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.
Customer status (dashboard row, /configs list, detail header + strip) is
now worst(controllerDerived, hostStatusOf(each expected host)) via the ONE
staleness definition (Server.hostStatus, hosts.go - shared with the
HostStalenessChecker; no second threshold). Any host down/stale caps the
customer at WARN with a cause chip naming the host ("host down: <id>");
pending (never-reported) hosts worsen only once the customer has reported
(onboarding exclusion). The three previously-inlined controller-status
chains collapse into controllerStatus() (rollup.go). Display + derivation
only - checker alerting untouched.
Live shape pinned (drill-1 / Peti cluster): host down 23h + controller
report minutes old rendered a GREEN row - TestRollup_DeadHostMasking now
fails that exact outcome. Red-proof: short-circuiting foldHostStatus to
controller-only flips C + two D subtests red ("dashboard row is GREEN
over a 23h-dead host").
- Auto-refresh: the 60s reload fires only while a live tab is active
(nav data-live-tabs="overview,applications,events,host") AND no form is
dirty (delegated input/change listener; never reset — a reload clears it).
Skipped ticks reschedule; a muted (paused) hint shows next to the toggle on
non-live tabs / dirty forms. Toggle, localStorage key, 60s cadence, and
default-on behavior unchanged. The refresh script resolves the legacy
settings→edit hash alias like the tabs script.
- Rider 4a: every template's stylesheet link is /style.css?v={{hubVersion}}
(the v0.47.0 gotcha: max-age=3600 served stale styling for up to an hour
after a deploy). Red-proof run: a reverted bare link fails the test.
- Rider 4b: CLAUDE.md standing rule — never git add -A in this repo
(the 146d165 sweep incident); explicit paths + pull-rebase + one writing
session per clone.
- Tests: Group C structural pins (attribute read, dirty listeners, alias x2,
hint element, cadence/key survivors) + Group D cache-bust over six pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZc5w5jDhFLv6qDC32KN5v
Endpoint card + peers table (truncated pubkeys with full-value title attr,
bound peers link to /hosts/<id>); Offsite nav link in all 9 page templates;
render tests for endpoint/peers, empty, and not-configured states.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
- static/fonts/: the 4 vendored woff2 (byte-copied from
felhom-controller), embedded (embed.go) and served at /static/fonts/
(font/woff2, immutable) mirroring the chart.min.js pattern. No CDN
before, none now.
- templates/icons.html: 12-symbol Lucide sprite partial (icon_sprite),
included at the top of <body> on all 9 pages ({{template}} — the hub
has no shared layout; per-page include is the minimal shared block).
- statusColor now returns v2 semantic tokens (nominal/warn/crit/
neutral) consumed as class suffixes: ok->nominal, warn+stale->warn,
down+fail->crit, pending+disabled->neutral (a not-yet-provisioned or
deliberately paused customer is a normal fleet state), blocked->warn
(intentional operator cut-off, attention-worthy not an outage),
unknown->neutral. The inline style="color: {{statusColor}}" pattern
is dead: dashboard + customer_unified render a class-based
.status-dot-<token>; statusIcon (constant "●") retired from funcmap
and templates.
- Tests (new; the hub web package had no funcmap/template tests):
TestStatusColorTruthTable over the full enumerated status set —
red-proven vs the old implementation (ok returned "#4ade80") — and
TestTemplatesParseWithFuncmap.
- Add Configuration page with "Refresh Assets" button
- Replace seedIfEmpty with seedOrUpdate (SHA-256 compare on startup)
- Translate all Hungarian text on Apps pages to English
- Add Configuration tab to all template navigation
- Expand isAssetFile to match favicon patterns
- Add felhom-logo.svg to website assets for the pipeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add hubVersion template function via closure in web.New(). Version is
passed from main.go (set via ldflags at build time) and displayed in
the footer of all 6 page templates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace separate config detail and report detail pages with unified
/customers/{id} page showing both config info and live report data
- Add "blocked" status for customers (hidden from dashboard, notifications
suppressed, still accepts reports)
- Dashboard now shows config-only customers as "PENDING" status
- Customers list: all rows link to /customers/{id}, show BLOCKED badge
- New actions: block/unblock, push config to controller, auto-create
config from report data
- /configs/{id} now redirects to /customers/{id}
- Add config-badge CSS classes for MANAGED/MANUAL/BLOCKED badges
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Customers page now shows ALL customers: both pre-configured (managed)
and report-only (manual) — merged from customer_configs + reports tables
- Renamed "Configurations" → "Customers" in navigation tabs
- Renamed "+ New Configuration" → "+ Add Customer"
- Status column with ok/warn/down badges, version column, managed/manual badge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New "Configurations" section lets operators pre-configure customer settings
in the Hub, then docker-setup.sh can download a ready-made controller.yaml
using just a customer ID and retrieval password.
- Store: customer_configs table with CRUD + per-customer API key lookup
- API: GET /api/v1/config/{id} with X-Retrieval-Password auth
- Auth: per-customer API keys alongside existing global key (backward compatible)
- Web UI: /configs list, create, edit, delete, YAML preview, copy-to-clipboard
- YAML gen: deep-merge controller.yaml.example template with customer overrides
- Template fetcher: background goroutine refreshing template from Gitea repo
- Navigation: Dashboard / Configurations tabs on all pages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Hub service receives reports from customer controllers
- SQLite store with 90-day retention and auto-prune
- REST API: POST /api/v1/report, GET /api/v1/customers
- Dark theme dashboard with status overview table
- Customer detail page with system, storage, containers, backup, health
- Bearer token auth for report ingest, bcrypt auth for dashboard
- K8s manifest for felhom-system namespace (Deployment, Service, Ingress, PVC)
- Dockerfile with multi-stage build
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>