Commit Graph

94 Commits

Author SHA1 Message Date
admin e35b1ae0e6 hub v0.46.0: observability pass — per-box log pulls, bundle custody, 72h TTL + secret gate
log_bundle_requests + log_bundles store (gzip, newest-3, 72h TTL purged on the
60s sweep); SaveLogBundle secret gate fail-closed (blocked flag row, no payload;
REDACTED/checksums pass). Report ACK gains controller_log_requested + ingests
controller_log_tail; heartbeat envelope gains log_tail_requested + ingests
log_tail (consume-once on arrival; pre-0.83 agents stay visibly pending). Host
detail Diagnostics section: request buttons (controller/agent), state rows with
honest latency hints, View/Download endpoint. Red-proofs: gate disabled and
clear-on-arrival removed both FAIL their tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-11 16:57:47 +02:00
admin bbecf0592e hub v0.45.0: floor-UI separation + effective-floor source + per-box MinAgent conditional floor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-11 15:33:14 +02:00
admin ce6a56691e hub v0.44.0: PBS DR tier SLICE 1 — felhom-tenantsync surface (script+client) + hub provisioning flow (consume-once host secret, pbs_dr desired-state descriptor, fail-closed + idempotent, re-issue)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-10 20:49:48 +02:00
admin 3e0c327f16 hub v0.43.1: Git Sync form hint — credentials optional, private catalog only (pairs with controller v0.112.0)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-10 17:53:52 +02:00
admin c084046af0 hub v0.43.0: remote app-log diagnostics — copyable issues + context + on-demand log tails + range/dismissal fixes
- store: app_log_issues gains context/context_customer (first capture wins) + dismissed_at (resurface only on last_seen > dismissed_at); log_tail_requests (pending operator intents, consume-once) + app_log_tails (transient, keep last 2 per app)
- api: /report ingests log_tails (stores + clears the request); ACK advertises log_tail_requests (same additive omit-when-empty pattern as escrow)
- web: Known Issues rows click-to-expand (full copyable message + context with provenance + explicit affected-customers list); Dismiss replaces Delete; period selector now filters issues (F); ?customer= filtered view + customer-page drill-down links (H); per-app Request-log-tail button + pending badge + App Log Tails section + ordered tail view with line numbers + .log download; customer-visible log_tail_requested event
- tests: store (context first-capture/late-adopt, range filter, dismissal old-window vs new-occurrence, tail request/fulfill/prune/scoping), api ACK round-trip, web render (expanded row, customer page sections, tail view + download + cross-customer 404)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-10 16:03:32 +02:00
admin 9f1759132d hub: configgen test pinning logging.level=debug propagation (v0.42.0 e2e leg)
Proves a customer ConfigJSON logging.level=debug deep-merges over the template
default into the generated controller.yaml — the integration leg the remote
debug-mode toggle depends on. Merge target (logging.level: info) verified live
on the demo controller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-10 15:21:55 +02:00
admin c918baa656 hub v0.42.0: remote "Debug mód" toggle on the customer config editor
Adds a form-level debug-mode checkbox to the customer config editor so an
operator can flip the controller's Logging.Level=debug (/debug menu + verbose
log) remotely, without SSH. Form field (not raw-JSON injection) because
handleConfigUpdate rebuilds ConfigJSON from the form on every save; the
config-version bump makes the controller re-pull + self-restart next cycle.

- buildConfigJSON: debug_mode checked -> "logging":{"level":"debug"};
  unchecked -> logging key omitted.
- config_form.html: "Hibakeresési mód (fejlesztői)" section + render state.
- configs_debug_test.go: form->JSON both ways; full-path survival test
  (debug lands, offsite descriptor unchanged, foreign-key red-proof); render
  state; red-proof exercised.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-10 15:14:43 +02:00
admin fad5573dd3 hub v0.41.0: OffsiteChecker (fill 90/95 + staleness >48h) + operator freeze lever (SLICE 4)
Sibling checker over the controller report's offsite object: quota-fill
warn/crit + the silently-stuck staleness detector (escrowed-only,
red-proofed; nil-safe on pre-v0.109 reports; same-second tie-guard).
SetOffsiteFrozen flips ONLY readonly on the exactly-1 labelled sub-account
(SSH preserved); Freeze/Unfreeze buttons — manual only, never automatic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-09 23:57:12 +02:00
admin 49d1233391 hub v0.40.0: store escrow restic_pw_sha256 + serve escrow status in the report ACK (SLICE 3)
Additive host_escrow migration; SaveHostEscrow/HostEscrow gain the hash
(NULL-safe for legacy rows); GetEscrowStatusForCustomer joins hosts;
the report ACK gains escrow:{identity_blob_present,restic_pw_sha256,
created_at} (omitted without a row). Contract test mirrors the agent's
v0.79.0 emit struct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-09 23:10:29 +02:00
admin 17cc67f7cd hub v0.39.0: offsite hardening — F4 credential re-issue, F2 scan retry, F5 save UX
F4: ReissueCredentials — explicit operator recovery for consumed-password
dead-ends; resets the labelled resource's password (exactly-1 guard,
red-proofed), stores a fresh one-time secret, bumps ConfigVersion.
New hetznerapi.ResetBoxPassword for the dedicated path.
F2: host-key scan retry-with-backoff (~60s ladder, red-proofed) — first
save survives fresh-subaccount DNS lag.
F5: config form disables submits + shows an in-flight notice (the re-click
bait that caused live F1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-09 22:39:08 +02:00
admin 7c70c545d9 hub v0.38.1: offsite provisioning must survive a client disconnect (live F1)
First supervised live run: the ~25s spinner-less offsite save invited a
re-click; the abandoned first request's r.Context() was canceled between
CreateSubaccount and SaveOneTimeSecret, stranding sub-account 268985 with a
password lost forever (consume 404s permanently).

applyOffsite now provisions on context.WithoutCancel + 3-minute absolute
timeout: once the create starts, create->wait->store runs to completion.
Regression test with a ctx-honoring fake that cancels the request context
mid-create; red-proofed against the raw-ctx pre-fix shape (reproduces the
exact live error).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-09 21:00:27 +02:00
admin 229650b4ee hub v0.38.0: offsite SLICE 2 — capture the box host-key fingerprint
Descriptor.HostFingerprint (SHA256, non-secret), captured at provision via an
x/crypto/ssh keyscan (SSHHostKeyScanner — dials :23, grabs the host key from the
handshake, no ssh binary). Fail-closed: nil scanner or scan failure → error (don't
serve a descriptor the controller can't verify). Pairs with controller v0.106.0
which re-scans + refuses on mismatch (no blind TOFU).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-09 19:14:36 +02:00
admin 44ec06b50f hub v0.37.0: offsite provisioning SLICE 1 — Cloud-API client + provisioning core
Hetzner storage-box provisioning against api.hetzner.com/v1 (NOT .cloud).
internal/hetznerapi (typed client + CloudAPI interface + Fake + WaitAction);
internal/offsite (Provisioner.ProvisionOffsite — idempotent by label, shared
sub-account/dedicated box, transient password, non-secret Descriptor,
fail-closed); one_time_secrets store (single-use Save/Consume); POST
/offsite/consume-password/{id} (customer-key auth, once); config-form Offsite
section → applyOffsite (502+no-save on error) → descriptor in ConfigJSON →
version bump. Token/passwords never logged/committed/in ConfigJSON. Tested vs a
faked Cloud API + fail-closed red-proof. NOT yet live-provisioned (needs the
dedicated-project scoped token; current token can delete ep0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-09 18:38:24 +02:00
admin 844fbfa749 GL-7 Part 2: install-command generator on the customer page
Replaces the static Option-1/2 Setup Command blocks with an interactive,
client-side builder: mode (required radio), cores/memory (required for byo,
optional for appliance), vmid, node, acl-storages (quote-wrapped),
operator-pubkey-file, preserve-state-from, and --dry-run/--preflight-only/
--skip-provision/--allow-new-leaf checkboxes. genFlags()/genUpdate() assemble
a live-updating download-then-run command (never curl|bash) + a local-run
variant, enforcing the script's own rules client-side (mode required; byo
requires caps → shows a warning + no runnable command; appliance hides the
caps requirement; allow-new-leaf shows its leaf-regen warning). Emits ONLY
real host-install v1.12.0 flags; the dangerous/operator-only set
(--force/--rotate-recovery/--enable-oob/--remove-golden/--uninstall/
--adopt-pool/--rescope-acl) is never offered. Graceful static fallback:
the server-rendered Option-1/2 commands keep --customer-id + a --mode
placeholder when JS is off. No framework/CDN/network; ScriptVersion (const,
in sync with SCRIPT_VERSION) drives the header. Render/structure test covers
the control ids, version, fallback, and the excluded-flag absence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-09 08:29:15 +02:00
admin b93dd03f0a GL-7 Part 1: passphrase hardening on the customer page (security)
The Setup Command panel rendered the per-customer retrieval passphrase (the
secret that fetches the WHOLE config) in cleartext twice — as #retrieval-pw
text and baked into the Option-3 curl's X-Retrieval-Password header — which
contradicts the panel's own "never on the command line" guidance. Now:
the retrieval password is MASKED by default (bullet run) with Reveal/Hide +
copy-secret controls (value lives in data-secret — the existing reveal
model); the Option-3 debug command carries a <YOUR-RETRIEVAL-PASSWORD>
placeholder, never the secret. Render test asserts the secret is not baked
into any command + is masked by default; red-proof (bake it back) FAILS.
(A zero-secret-in-DOM reveal-on-demand fetch is a noted follow-up, not this task.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-09 08:23:41 +02:00
admin f8fc09e5cc feat(hub): OOB access health ingest + degraded alert (H1 Part 4)
store.GetHostOOBStates parses the agent oob heartbeat stanza. monitor/host_oob:
transition-based oob_degraded/oob_recovered warning (felhom-sshd down while the
operator peer is configured, OR config invalid) — proactive "can the operator get
in right now" signal; unconfigured OOB never alerts. Wired into the 60s sweep.
Non-hollow tests + transitions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-05 22:30:03 +02:00
admin 0ec7555126 feat(hub): operator OOB peer + oob_peer_ip desired-state merge (H1 Part 1)
store.SetOperatorOOBPeer/GetOperatorOOBPeer (empty-host_id wg_peers row, explicit
/32, validated in-subnet/not-reserved/not-taken, last-write-wins rotation).
PUT/GET /admin/wg/operator-peer (global key). mergeWireguard adds oob_peer_ip when
an operator peer exists (absent = byte-identical). BumpAllHostGenerations forces
fleet re-fetch. Non-hollow tests both sides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-05 22:08:06 +02:00
admin 012e5f3ecc fix(hub): mgmt_plane_healed alerts on the FIRST auto-heal (TASK G1) — v0.34.1
A heal marker is an event, not a baseline: construction seeds pre-existing markers
(startup false-alarm guard) but a newly-observed marker now raises the warning, so
the first auto-heal surfaces (matches the live drill). Added tests for both halves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-05 19:12:50 +02:00
admin 05d81810d4 feat(hub,install): break-glass recovery vault + mgmt_plane surfacing (TASK G1)
Hub half of the management-plane break-glass (prereq for felhom-sshd/H1; agent
half = felhom-agent v0.71.0). Closes SPIKE-felhom-sshd §8/#9.

- store.host_recovery + methods: per-host root@pam console password, at-rest,
  operator-retrievable (the PVE-web-console fallback when sshd + auto-heal both fail).
- API: PUT /hosts/{id}/recovery-credential (self-scoped, day-0 vaults) + GET
  /admin/hosts/{id}/recovery-credential (global key only). Secret never logged
  (red-proofed).
- monitor/host_mgmtplane: parses the agent mgmt_plane stanza, raises
  mgmt_plane_healed WARNING on a new privsep_healed_at (recurring clobber surfaces
  before lockout; complements host_staleness).
- host-install: step_break_glass generates a strong root@pam password (openssl
  rand, never logged/filed — stdin to chpasswd + curl), vaults via host key;
  idempotent unless --rotate-recovery. Installs the G1 host artifacts (tmpfiles +
  agent-independent watchdog timer), RuntimeDirectory-guarded; uninstall removes them.

Hub v0.34.0. Non-hollow tests + red-proofs; full suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-05 19:03:18 +02:00
admin 13203c2452 hub: S2 /offsite registry page (read-only) + nav + WGPeer.CreatedAt
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
2026-07-04 00:48:03 +02:00
admin ba52005e61 hub: S2 API — box-facing WG registration + merge-at-read + hub-owned-key guard
POST /hosts/{id}/wg (per-host self-scoped; global = operator/DR path): bind /
re-key-in-place / adopt; generation bump + endpoint push ONLY on real change.
mergeWireguard injects the hub-owned block into served desired-state at READ
time (stored operator blob never touched; fail-safe unmerged on any error;
no-peer = byte-identical pass-through — existing golden test untouched+green).
handleAdminSetDesiredState rejects top-level wireguard (400). Admin DELETE of a
BOUND peer bumps the owning host. NEW golden desired-state-wireguard.golden.json
= the S3 cross-repo contract. Red-proofs a/b/c/d run + reverted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-04 00:44:40 +02:00
admin fcf84a0c5c hub: S2 store — host-bound WG peers (register/re-key/adopt), one-per-host index, BumpHostDesired
allocateWGPeerTx extracted from addWGPeerOnce (behavior-neutral; S1 tests
unmodified+green). RegisterWGPeerForHost: idempotent / re-key-in-place-keep-ip /
adopt-unbound / ErrWGPubkeyBoundElsewhere. Partial unique index enforces one
bound peer per host. BumpHostDesired touches ONLY the generation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-04 00:39:24 +02:00
admin 0fa7ea1da1 hub: wgsync fix — constrain HostKeyAlgorithms to the pinned key's type
Live S1 validation caught it: a stock multi-hostkey sshd presented ECDSA while
we pin ed25519 → FixedHostKey refused a legitimate server. Regression test with
an in-process dual-hostkey server (fails without the fix — red-proofed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-03 23:52:08 +02:00
admin fbeeacb124 hub: S1 wgsync (pinned-SSH push + declarative reconciler) + /admin/wg API + env wiring
internal/wgsync: x/crypto/ssh client with ssh.FixedHostKey pin (no insecure
fallback), forced-command exec, ok/applied response contract; Reconciler pushes
the FULL peer list on Trigger or 5-min tick (drift repair by construction).
internal/api/wg.go: PUT/GET /admin/wg/endpoint + POST/DELETE/GET /admin/wg/peers,
global-key-only, pubkey in body (base64 vs URL), sync ok|deferred|disabled.
main.go: WG_ENDPOINT_SSH_* env wiring, disabled-with-INFO when unconfigured.
Groups B/C/D tests incl. in-process SSH server; red-proofs b/c/d run + reverted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-03 23:40:22 +02:00
admin b18f6aee1b hub: S1 store — wg_endpoints/wg_peers tables + /32 allocator (doc 06 §3.2)
Additive migration; AddWGPeer = one tx, idempotent on pubkey, lowest-free-host
allocation skipping network/pbs_tunnel_ip/broadcast, UNIQUE(assigned_ip) race
backstop with one internal retry; typed ErrWGEndpointUnset/ErrWGSubnetExhausted.
Group-A tests + red-proof (allocator-ignores-rows mutation fails 3 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-03 23:32:39 +02:00
admin ad61e96abd hub: render-test for the dashboard critical badge (Scenario D — UI verify; operator login gates Chrome)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-03 11:15:15 +02:00
admin b5f00509ee hub v0.31.0: accept 'critical' severity at event ingest + UI badges/CSS; event_test.go (red-proofed); REUSE.md §1/§3 updated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-03 11:08:44 +02:00
admin d3fdbbc5ad hub v0.30.1: status badges no-wrap (PENDING/NO REPORT wrapped in narrow columns) 2026-07-02 22:52:31 +02:00
admin c136308aad D4 Part 2: hub style.css v2 + template sweep
- style.css: slate tokens -> the canonical navy block + @font-face
  (self-hosted, latin-ext for Hungarian customer names); single 2px
  radius; hairline table rows (--line-soft dividers, density kept);
  status classes re-expressed per the design-system addendum:
  .status-badge = outline tag + CSS ::before dot keyed by the raw
  status the templates emit (ok=blue, warn/blocked/stale=amber,
  down/fail=red, pending/disabled=quiet neutral); .status-dot =
  class-based 8px dot; severity badges stay filled amber/red
  (exceptions stay loud); config badges = filled informational chips
  in v2 colors; row tint only for warn/down (warn-dim/crit-dim).
- Two-tone brand heading on all 8 'Felhom Hub' pages
  (<h1>Felhom <span>Hub</span></h1>, span = blue-bright).
- app_detail chart retinted: avg #2EA8F5 (primary data), peak #8E7CE8
  (secondary DATA series — not status red), catalog-limit line #E0A93E
  (threshold marker); legend/tick/grid -> v2 literals.
- customer_unified: JS status-message colors (success -> blue-bright,
  error -> crit), all inline slate hexes -> tokens.
- Login page inline HTML retinted (#0088cc -> #0083D8).
- Grep gate: every slate hex (#0f172a #1e293b #334155 #60a5fa #4ade80
  #facc15 #f87171 #94a3b8 #64748b #475569 #e2e8f0) at ZERO across
  hub/internal/web (non-test); statusIcon 0; inline statusColor
  style 0. go build/vet/test green; api/ + store/ untouched.
2026-07-02 22:38:37 +02:00
admin bc8d54df6a D4 Part 1: hub fonts + sprite + statusColor semantic remap
- 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.
2026-07-02 22:34:40 +02:00
admin 079a2cdd08 hub v0.29.0: Day-0 artifact manifest — version dropdowns + auto-derived sha
Operator picks a version from a Gitea-populated dropdown; the hub reads that
version's sha256 from Gitea itself (files-metadata API, no artifact download) and
vouches it — no hand-copied checksums. New internal/gitea read-only client
(ListVersions + FileSHA256, unit-tested). Configuration UI: version <select>s +
read-only sha display; handleSetArtifacts derives the sha authoritatively and
refuses the save on a Gitea lookup failure. Degrades to manual text entry without
registry creds. go build/vet/test clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:02:13 +02:00
admin 562c0dfeb6 hub v0.28.0 + host-install v1.2.0: settings→Configuration, online install, vmid auto-detect
Part A (hub): move the global-floor + Day-0-artifacts cards from the Customers
page to the Configuration tab; routes → /configuration/{global-floor,artifacts};
redirects + flashes to /configuration. Customers page back to list + Add.

Part B: online setup command on the customer page (download-then-run, passphrase
at prompt, not templated); serve /scripts/ from the website (sparse-checkout +
nginx location) so felhom.eu/scripts/felhom-host-install.sh resolves; script
passphrase prompt reads < /dev/tty (works for pipe-to-bash too).

Part C (script): --vmid auto-detect — default 9201 in use + no --force → pick the
next free id from pct+qm and confirm; explicit --vmid stays die-unless-force.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 06:43:29 +02:00
admin 2289fc907c hub v0.27.0: Hosts page — read-only fleet view (audit F-M1)
New Hosts nav section: a fleet list (/hosts) + per-host detail (/hosts/{id}),
read-only (GET only, no host actions). Surfaces identity, agent version,
online/stale status (reusing the HostStalenessChecker threshold), guests,
vitals, storage targets with SMART/thin-pool, and DR/escrow presence.

- store: new ListGuestsForHost reader (reality cols only; omits api_key/
  desired_spec_json) + scanGuest helper.
- web: handleHostsList + handleHostDetail (hosts.go); hosts.html +
  host_detail.html; Hosts nav link on every page; timeAgoPtr helper; routes.
- tests: store getter, both handlers, no-secret (api_key) assertion, 404,
  no-report empty state, status-band mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 06:10:02 +02:00
admin a3ac6c9488 hub v0.26.0: pull-based config delivery + retire inbound GUI controls
config_version counter (bumped on every config save) advertised in the report
ACK; controller re-pulls + self-restarts on a change. Retire Trigger Update /
Push Config / Pull Config / Show Diff handlers+routes+buttons and the inbound
geo-notify (keep hub->Cloudflare geo removal). Setup command -> host-install;
delete dead customer.html + config_detail.html. Closes AUDIT-hub-gui F-S1/F-S4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
2026-06-30 21:49:33 +02:00
admin 88073ac464 hub v0.25.0: per-storage worst-fill alerting (StorageFillChecker)
Generalizes host_disk to any reported storage target (dump/backup volume, data drive,
thin pool, PBS). Per-(host,target) state, born/persistent, natural critical severity,
distinct storage_fill_* events; excludes the root-backed builtin (host_disk owns root).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
2026-06-30 19:01:20 +02:00
admin 0ff1d3c883 hub v0.24.0: dispatcher routes critical severity (+ nil-prefs crash guard)
ProcessEvent routed only warning/error; a critical-severity event was silently dropped.
Now routes warning/error/critical, logs unrecognized severities, and guards a nil
GetNotificationPrefs (which would panic/crash the hub). host_disk_critical emits its
natural critical severity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
2026-06-30 14:18:45 +02:00
admin 897997c164 hub v0.23.0: host root-disk pressure monitoring + alert
New HostDiskChecker on the 60s sweep alerts the operator when a Proxmox host root
filesystem crosses warn (90%) / crit (95%). Born/persistent (a disk already full at
hub restart alerts on cycle 1); distinct host_disk_* event types from the guest disk_*;
critical band maps to severity error (the dispatcher only routes warning/error).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
2026-06-30 13:48:55 +02:00
admin 9c5cf2975f hub: HostLeafChecker — proactive agent re-key detection (host_leaf_changed) v0.22.0
Watches each host's reported local-API leaf fp; alerts on change (trust-on-first-report). Sibling of
HostCapabilityChecker; store.GetHostLeafFingerprints reads report_json (no migration); hub-generated
event (no allowlist change). Change red-proof + first-obs-seed + empty-skip + blocked-drop tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pg8ANF97SEeKYSN5Jxw3qJ
2026-06-29 23:15:22 +02:00
admin 12f038618e hub: F2 — alert on host already degraded/stale at (re)start (seed only healthy) v0.21.0
Constructors seed only healthy hosts; an already-degraded/stale host is left unseeded so the first
Check() emits once (cooldown dedups on hub restart). Born-degraded red-proof + staleness test updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pg8ANF97SEeKYSN5Jxw3qJ
2026-06-29 21:46:25 +02:00
admin 121405772f hub: accept controller agent_channel_* events (channel-health relay) v0.20.0
The controller v0.90.0 channel-health checker pushes agent_channel_* events to /api/v1/event;
the allowedEventTypes allowlist rejected them (HTTP 400). Added the 8 types (operator-only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPZ4GJ8L5Jqf8UiPwbn1kt
2026-06-29 20:37:24 +02:00
admin b7b165bff5 hub: HostCapabilityChecker — operator alert on agent capability-degraded (v0.19.0)
Companion to felhom-agent v0.44.0. New monitor.HostCapabilityChecker (sibling of
HostStalenessChecker) reads the capabilities snapshot from the latest host report and emits
agent_capability_degraded/recovered (operator-only, 1h cooldown) on ok<->degraded transitions
for any Critical capability. store.GetHostCapabilities (MAX(id), no migration). Goldens mirror
the new capabilities field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPZ4GJ8L5Jqf8UiPwbn1kt
2026-06-29 18:50:50 +02:00
admin fa3c4f2657 hub v0.18.0: app-email passthrough POST /api/v1/mail → Resend SMTP
Raw-MIME passthrough (STARTTLS, AUTH LOGIN) — separate from the notify HTTP-API
alert path (which drops inline CID images). Per-customer token-bucket rate limit,
From-header allowlist backstop. Resend key stays hub-side. No new external dep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 08:45:22 +02:00
admin 39ef64e128 hub v0.16.0 + host-install v1.1.0: Day-0 artifact manifest + self-install the agent (BUNDLE slice)
Hub (v0.16.0):
- store: ArtifactManifest{agent,golden version+sha256} in hub_settings; Get/SetArtifactManifest.
- handler: GET /api/v1/artifacts/{id} (passphrase auth, mirrors config-retrieve). Unset => 200 empty.
- web: operator UI "Day-0 artifacts" card (POST /configs/artifacts), semver + 64-hex validation.
- artifact_test.go: returned-verbatim / unset-empty / 401 / 404 / store round-trip.

host-install (v1.1.0):
- new step 5/8 agent-install: manifest + git token (config-retrieve) -> fetch binary from Gitea ->
  verify sha256 vs hub manifest (abort on mismatch) -> install non-root felhom-agent user + binary +
  sudoers (visudo -cf) + canonical unit. Idempotent.
- new step 7/8 golden: local fallback else fetch+verify+import from Gitea (--force-gitea-golden).
- agent now runs non-root (privileged.mode sudo), config chowned to the service user.
- README prerequisites trimmed to: install PVE + create customer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:38:26 +02:00
admin 30380a59f4 hub v0.15.0: Phase 2 managed updates — per-customer controller-version floor
Operator sets a minimum controller version (FLOOR), per-customer defaulting to a
global floor; the report ACK returns the effective floor + latest_version so the
controller auto-updates to the floor when below it (latest stays the opt-in button).

- store: min_controller_version column + hub_settings global floor + Effective/
  Get/SetGlobal/SetMin resolution + config/env DEFAULT_MIN_CONTROLLER_VERSION
- handler: report ACK {min_controller_version, latest_version}; LatestVersionProvider
- web: global floor editor + per-customer override form + Floor column (English)
- tests: floor resolution + ACK + render; override-precedence red-proof verified

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSZmmSFVzGwEzhYmxbkgBK
2026-06-27 11:59:30 +02:00
admin 8098237ce1 hub v0.14.0: passphrase-authed host enrollment (Day-0 option C)
New POST /api/v1/host-enroll (handleHostEnroll): X-Retrieval-Password authed,
body {customer_id} -> {host_id, api_key}. Mint-once-reuse (201 first, 200
reuse) so re-running the host-bootstrap never orphans a running agent's key;
auth checked before any mint. Backed by new Store.GetHostByCustomer
(ORDER BY updated_at DESC LIMIT 1, idx_hosts_customer).

GET /config/{id} and global-key POST /admin/hosts left untouched. Exact-match
route (path == "/host-enroll") to avoid the /hosts/ prefix collision.

Tests: host_enroll_test.go (mint/reuse/401-no-mint/404/400) + GetHostByCustomer
store test; companion red-proof verified always-mint fails the reuse assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtXesNa2LGbMmE4DNL6SE7
2026-06-26 15:35:24 +02:00
admin 149a3b092e hub v0.13.1 — DR recipe v1 drive-shape sync (test-data + regression guard only)
Tracks felhom-agent v0.39.0, which dropped role + restic_repo_coord from the host-half
drives[]. Hub reads drives as json.RawMessage (verbatim passthrough) → no store/handler
change needed. Dropped role from the store golden + drHostHalf fixture; re-synced the api
host-report golden byte-identical with the agent copy (it previously lacked dr_recipe
entirely). Added TestAssembleDRRecipe_V1DriveShape regression guard (demonstrated to fail
when the fixture re-adds role, then reverted). No behavior change; redeploy optional.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:25:51 +02:00
admin 5f5e3c54a1 hub v0.13.0: DR recipe — assemble + store + view the secret-free reconstruction recipe
DR recipe slice (hub half), grounded in SPIKE-dr-recipe-2026-06-16. The hub
receives two additive dr_recipe halves on the existing report paths (agent
storage/guest/PBS on host-report; controller customer/apps on the controller
report), stores them PLAINTEXT in a DEDICATED dr_recipe table keyed by customer
(each half preserves the other), and AssembleDRRecipe stitches them into one
operator-readable recipe (ignore-unknown + version-skew tolerant).

View: a DR-recipe panel on the customer page + GET /customers/{id}/dr-recipe.json
download (operator-auth, no secrets to redact). Plaintext-at-rest is correct —
the recipe is the clean inverse of the retired infra-backup.

Tests: store round-trip (each half preserves the other), assemble-matches-golden,
ignore-unknown + version skew, partial halves, no-secrets sweep. Manifest tag
bumped to v0.13.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:49:45 +02:00
admin 0635640848 hub v0.12.0: retire Infra Backup, purge its plaintext secrets, fix backup-deadline email
Phase-1 of SPIKE-infra-backup-2026-06-15. The infra-backup mechanism was dead
since slice 8C yet stored plaintext customer secrets at rest (app-secret key,
restic password, Cloudflare tokens) — a zero-knowledge violation — and its
absence made the daily expected_backup_missed email fire for healthy customers.

- Repoint monitor.CheckBackupDeadlines backup half to the agent host-report's
  PBS snapshots (+vzdump): alarm only on no-backup / >26h stale / verify failed.
  Keep the db_dump half. No host-report → no backup alarm (liveness owns that).
  New store.GetLatestHostReportJSON. Tests incl. a companion that fails pre-fix.
- Remove the infra-backup endpoints, store methods/types, and operator panel;
  /recovery now returns config_yaml only.
- migrate(): DROP infra_backup_versions/infra_backups + VACUUM (+wal_checkpoint)
  to physically reclaim the plaintext pages, gated on table existence.

Flagged out-of-scope: exposed creds need operator rotation; legacy reports table
holds historical plaintext restic_password rows (separate leak, not purged here).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:08:06 +02:00
admin 3457415117 slice 10D (hub): DR capstone — recovery mode + re-enroll + directive serving (hub v0.11.0)
Recovery-mode toggle (global key, bounded auto-expiry) gates re-enroll +
restore-directive serving. Re-enroll rotates the agent<->hub credential to the
new box (old key revoked); returns the opaque escrow blobs + non-secret
directive. Store gains recovery_mode_until + identity_blob + directive_json.
Hub holds no usable secret + no Cloudflare write-power (operator-side rotation).
Doc 03 §9: slice 10 CLOSED.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 09:48:38 +02:00
admin 0c843286a2 slice 10B: signed-op job completion (DELETE clear-job) (hub v0.10.0)
Add DELETE /hosts/{id}/jobs/{job_id} (per-host self-scoped, idempotent) so the
agent clears a job after executing or terminally rejecting it. The hub stores
the operator-signed blobs opaquely (no signing key — cannot forge or open);
the agent verifies + executes. Doc 03 §4/§6/§9 updated (operator-signed path
live; 8C wipe completes; 10B done).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:14:32 +02:00