{{template "inline_confirm_js"}} {{template "icon_sprite"}}
← All Customers

{{if .CustomerName}}{{.CustomerName}}{{else}}{{.CustomerID}}{{end}} {{if .HostCause}}{{.HostCause}}{{end}}

{{if .HasReports}}

Last report: {{timeAgo .Customer.ReceivedAt}} · Controller {{.Customer.ControllerVersion}}

{{else}}

No reports received yet

{{end}}
{{if .Flash}}
{{if eq .Flash "created"}}Configuration created successfully. {{else if eq .Flash "updated"}}Configuration updated. {{else if eq .Flash "password_regenerated"}}Retrieval password regenerated. {{else if eq .Flash "offsite_reissued"}}Offsite credentials re-issued — a fresh one-time password is staged; the controller picks it up on its next config refresh. {{else if eq .Flash "pbsdr_reissued"}}PBS DR credentials re-issued — a fresh one-time token secret is staged for the agent. Confirm it actually landed: the host's pvesm status must show the PBS entry active. A converged agent can report applied while the storage still authenticates 401 (R-39). {{else if eq .Flash "offsite_frozen"}}Offsite storage FROZEN (read-only) — new backups and prune will fail until unfrozen. {{else if eq .Flash "offsite_unfrozen"}}Offsite storage unfrozen — read-write restored. {{else if eq .Flash "blocked"}}Customer blocked — hidden from Dashboard. {{else if eq .Flash "unblocked"}}Customer unblocked — visible on Dashboard again. {{else if eq .Flash "log_tail_requested"}}Log tail requested — the controller delivers it on its next report cycle (a few minutes). A customer-visible event line was recorded. {{else if eq .Flash "claim-resent"}}Code re-sent to the registered address. A kód a doboz következő jelentésekor (~15 percen belül) aktiválódik. {{else if eq .Flash "claim-resend-failed"}}Claim code resend FAILED — check the hub log (email delivery / send error). {{else if eq .Flash "selfbind-sent"}}Self-bind link sent to the registered address — valid for 7 days. The customer enters the box's console pairing code + their retrieval passphrase; no operator bind needed. {{else if eq .Flash "selfbind-no-email"}}Self-bind link NOT sent — this customer has no registered email address. Set one first, or bind the appliance manually from the Hosts page. {{else if eq .Flash "selfbind-send-failed"}}Self-bind link send FAILED — the link was invalidated (not left live). Check the hub log (email delivery / send error). {{else if eq .Flash "reset_done"}}Customer RESET complete — every operational trace was destroyed (offsite repo, PBS namespace, DR recipe, claim state, retained escrow custody). Identity and basic config survive; the audit event stream records it. {{end}}
{{end}} {{if .IsBlocked}}
This customer is blocked — reports are accepted but not shown on the Dashboard.
{{end}} {{if .StaleSinceReset}}
No data since the RESET ({{.ResetAt}}) — „RESET óta nincs adat". Every health figure below predates it and describes a lifecycle that no longer exists. The box repopulates this page on its first report after re-onboarding.
{{end}} {{if .OffsiteUnprovisioned}}
Offsite is enabled but was never provisioned — no descriptor exists for this customer, so nothing is being backed up offsite. Provisioning is Save-triggered: open the Edit tab and press Save once to provision it, then verify on the Offsite page. Re-enrollment will not fix this on its own — the auto-re-issue deliberately skips an unprovisioned target. (R-36)
{{end}}
{{if .CustomerName}}{{.CustomerName}}{{else}}{{.CustomerID}}{{end}} {{.OverallStatus}} {{if .HostCause}}{{.HostCause}}{{end}} {{if .HasReports}} Controller {{.Customer.ControllerVersion}} Last report {{timeAgo .Customer.ReceivedAt}} Containers {{.Customer.ContainerRunning}}/{{.Customer.ContainerTotal}} {{end}}
{{if not .HasReports}} {{if .HasConfig}}

Waiting for First Report

This customer has been configured but no controller report has been received yet.

Use one of the setup commands below to deploy the controller on the customer node.

{{end}} {{end}}

Customer Info

{{/* v0.48.0 edit-a: Edit/Block/Delete moved to the Edit tab (form + danger zone); only the config-less bootstrap action stays in the header. */}} {{if not .HasConfig}}
{{.CSRFField}}
{{end}}
Customer ID {{.CustomerID}}
Name {{if .CustomerName}}{{.CustomerName}}{{else}}—{{end}}
Domain {{if .Domain}}{{.Domain}}{{else}}—{{end}}
Email {{if .Email}}{{.Email}}{{else}}—{{end}}
Config {{if .HasConfig}} MANAGED {{else}} MANUAL {{end}} {{if .IsBlocked}}BLOCKED{{end}}
{{if .HasConfig}}
Config Created {{timeAgo .Config.CreatedAt}}
{{end}}
{{if .HasReports}}

Health

{{if eq .OverallStatus "disabled"}}

Reporting has been disabled on this node

Enable it in the controller's controller.yaml: hub.enabled: true

{{else if eq .OverallStatus "blocked"}}

Customer is blocked

{{else}} {{with .Report.health}}

Status: {{index . "status"}}

{{$issues := index . "issues"}} {{if $issues}}

Issues

    {{range $issues}}
  • {{.}}
  • {{end}}
{{end}} {{$warnings := index . "warnings"}} {{if $warnings}}

Warnings

    {{range $warnings}}
  • {{.}}
  • {{end}}
{{end}} {{end}} {{end}}

System

{{with .Report.system}}
Hostname {{index . "hostname"}}
OS {{index . "os"}}
Kernel {{index . "kernel"}}
CPU {{index . "cpu_model"}} ({{index . "cpu_cores"}} cores)
{{end}}
CPU {{formatFloat .Customer.CPUPercent}}%
Memory {{formatFloat .Customer.MemoryPercent}}%

Storage

{{with .Report.storage}}
{{range .}}
{{with index . "label"}}{{.}}{{else}}{{index . "mount"}}{{end}} {{printf "%.0f" (index . "percent")}}%
{{printf "%.1f" (index . "used_gb")}} / {{printf "%.1f" (index . "total_gb")}} GB
{{end}}
{{end}}

Backup

{{with .Report.backup}}
Enabled {{if index . "enabled"}}Yes{{else}}No{{end}}
Snapshots {{index . "snapshot_count"}}
Repo Size {{index . "repo_size_mb"}} MB
Integrity {{if index . "integrity_ok"}}OK{{else}}Unknown{{end}}
{{end}}
{{end}}
{{if .HasReports}}

Containers ({{.Customer.ContainerRunning}}/{{.Customer.ContainerTotal}})

{{with .Report.containers}} {{$list := index . "list"}} {{if $list}} {{range $list}} {{end}}
Name State CPU Memory
{{index . "name"}} {{index . "state"}} {{printf "%.1f" (index . "cpu_percent")}}% {{printf "%.0f" (index . "memory_mb")}} MB
{{end}} {{end}}
{{if .HasAppTelemetry}}

App Telemetry (last 7 days)

{{range .AppTelemetry}} {{end}}
App Memory (current) Memory (avg 7d) Memory (peak 7d) Catalog Limit Errors Warnings Logs
{{if .DisplayName}}{{.DisplayName}}{{else}}{{.AppName}}{{end}} {{formatFloat .MemoryCurrentMB}} MB {{formatFloat .MemoryAvgMB}} MB {{formatFloat .MemoryPeakMB}} MB {{if .CatalogLimit}}{{.CatalogLimit}}{{else}}—{{end}} {{if gt .LogErrors 0}}{{.LogErrors}}{{else}}0{{end}} {{if gt .LogWarnings 0}}{{.LogWarnings}}{{else}}0{{end}} {{if index $.PendingTails .AppName}} tail pending {{else}}
{{end}}
{{end}} {{if .HasLogTails}}

App Log Tails (on-demand, last 2 per app kept)

{{range .LogTails}} {{end}}
App Collected Received Lines
{{.AppName}} {{.CollectedAt.Format "2006-01-02 15:04:05"}} ({{timeAgo .CollectedAt}}) {{timeAgo .ReceivedAt}} {{len .Lines}} View Download .log
{{end}} {{else}}

Container and app data appear once the first report arrives.

{{end}}
{{if .HasConfig}}

Credentials

Retrieval Password
••••••••••••••••
The per-customer secret that fetches the whole config — masked by default; never place it on a command line (the installer reads it at a no-echo prompt).
{{.CSRFField}}
API Key
{{.Config.APIKey}}
Used by the controller for ongoing hub communication (reports, notifications, backups)

Dashboard access — customer claim

Claim status
{{if .Claim}} {{if .Claim.ClaimedAt}} Claimed {{timeAgoPtr .Claim.ClaimedAt}} {{else if .Claim.EmailedAt}} Nyitott — kód kiküldve {{timeAgoPtr .Claim.EmailedAt}} {{else}} Nyitott — a kód e-mail NEM ment ki (resend!) {{end}} generation {{.Claim.Generation}} · issued {{timeAgo .Claim.IssuedAt}} {{else}} no code issued yet issued automatically at the first config pull or report {{end}}
The customer sets + owns the dashboard password (claim code → own password). The code goes ONLY to the registered address ({{.Email}}); the hub stores a hash — no plaintext code exists to display.
{{if .Claim}}
{{.CSRFField}}
{{end}}
Let the customer bind their own freshly-installed appliance — no operator bind needed. Sends a 7-day capability link to the registered address ({{.Email}}); the customer opens it and enters the box's console pairing code + their retrieval passphrase. Wrong entries lock the link after 5 attempts. If the link expires, bind the appliance manually from the Hosts page.
{{.CSRFField}}

Setup Command

Day-0 host bootstrap. The command always fetches the current felhom-host-install.sh from felhom.eu — there is no version to pick here. Run it on a freshly-PVE-installed Proxmox host as root (create the customer in the hub first). It enrolls the host, installs + verifies the agent, and provisions the guest; the in-guest controller then pulls its own controller.yaml. The retrieval passphrase is entered at the no-echo prompt — never on the command line.

byo: required — a conservative slice of the host
byo: required — MiB (32 GB → 32768)
pick from pct list+qm list; blank = default/auto
required only on a multi-node cluster
grant the token write access on exactly these storages
arm self-update from day-0
reinstall keeping the leaf pin stable

Option 1: Online install (recommended)

Download-then-run (not curl | sudo bash) so you can inspect the script first — the right default for a sovereignty product. The passphrase is entered at the no-echo prompt.

curl -fsSL https://felhom.eu/scripts/felhom-host-install.sh -o felhom-host-install.sh \ && sudo bash felhom-host-install.sh --customer-id {{.CustomerID}} --mode <appliance|byo>

Option 2: Local install (script already on host)

sudo ./felhom-host-install.sh --customer-id {{.CustomerID}} --mode <appliance|byo>

Option 3: Manual config fetch (debug only)

The same payload the controller pulls itself — for inspection, not normal provisioning. Replace the placeholder with the Retrieval Password above (Reveal to see it) — it is intentionally NOT baked into this command.

curl -fsSL https://hub.felhom.eu/api/v1/config/{{.CustomerID}} -H "X-Retrieval-Password: <YOUR-RETRIEVAL-PASSWORD>" -o controller.yaml

YAML Preview

Loading preview...

{{else}}

No managed config yet — create one from the Overview tab to get setup commands.

{{end}}
{{if .HasConfig}} {{template "config_form_body" .ConfigForm}} {{else}}

No managed config yet — create one with the Create Config button above.

{{end}} {{if .HasReports}}

Controller Update

Controller version {{.Customer.ControllerVersion}}
{{if .LatestVersion}}
Registry latest v{{.LatestVersion}} {{if .UpdateAvailable}} ● update available {{else}} — up to date {{end}}
{{end}} {{if .ControllerURL}}
Controller URL {{.ControllerURL}}
{{end}}
Effective floor (min. version) {{if .EffectiveFloor}}v{{.EffectiveFloor}} {{if .BelowFloor}}● below floor — will auto-update {{else}}— at/above floor{{end}} {{else}}none (Phase 2 inert){{end}}
Global floor {{if .GlobalFloor}}v{{.GlobalFloor}}{{else}}unset{{end}}
{{.CSRFField}} Boxes below the effective floor auto-update on their next report. Blank clears the override.

Controller updates are agent-driven (the version floor above) and config is delivered by the box pulling it on a config change — the hub never connects into the box. Edit the config in the form above; the controller re-pulls and restarts on its next report.

{{with .Report.geo_restriction}}

Geo-korlátozás

Állapot {{if index . "enabled"}} Aktív {{else}} Inaktív {{end}}
{{if index . "enabled"}}
Engedélyezett országok {{$countries := index . "allowed_countries"}} {{if $countries}} {{range $i, $c := $countries}}{{if $i}}, {{end}}{{$c}}{{end}} {{else}} — {{end}}
{{end}} {{if index . "last_sync"}}
Utolsó szinkron {{index . "last_sync"}}
{{end}} {{if index . "last_sync_error"}}
Szinkron hiba {{index . "last_sync_error"}}
{{end}}
{{$overrides := index . "app_overrides"}} {{if $overrides}}

Alkalmazás felülírások

{{range $app, $override := $overrides}} {{end}}
AlkalmazásEngedélyezett országok
{{$app}} {{$ac := index $override "allowed_countries"}} {{if $ac}}{{range $i, $c := $ac}}{{if $i}}, {{end}}{{$c}}{{end}}{{else}}—{{end}}
{{end}} {{if index . "enabled"}}
{{end}}
{{end}} {{else}}

Controller update and geo-restriction settings appear once the first report arrives.

{{end}} {{if .HasConfig}}

Ügyfél-visszaállítás (RESET — pre-első-telepítés)

Egyetlen művelettel visszaállítja az ügyfelet az első telepítés előtti állapotba: minden működési állapot törlődik (offsite tároló, PBS névtér, DR-recept, egyszeri jelszó, azonosítási állapot). Az azonosság és az alapkonfiguráció megmarad (ügyfélrekord, előzmények, események). Ez NEM törli a hostokat — ha még van host, előbb azt kell törölni. Kevesebb, mint a Danger zone Delete: az ügyfél megmarad, csak a működési nyomok tűnnek el.

{{end}} {{if .Deletable}}

Danger zone

{{if not .HasConfig}}Ghost customer — the configuration record is already gone; Delete is the applicable action. {{end}}Blocking hides the customer from the Dashboard (reports are still accepted). Delete customer is the full offboarding teardown (v0.69.0): it deletes the host(s), then RESETs the customer (offsite repository destroyed, PBS credentials revoked, tunnel and zone removed), then purges the customer record and all escrow ciphertext — including the retained recovery-key custody for this customer's hosts. This is the one true purge point; host deletion only demotes custody, never destroys it. Three acknowledgements and the typed customer-id are required. For identity-preserving re-onboarding use Ügyfél-visszaállítás (RESET) above instead.

{{if .HasConfig}}{{/* v0.70.1: blocking gates dashboard visibility of a CONFIGURED customer — meaningless for a ghost */}} {{if .IsBlocked}}
{{.CSRFField}}
{{else}}
{{.CSRFField}}
{{end}} {{end}}
{{end}}
{{if .HasReports}} {{if .HasDRRecipe}}

DR Recipe (secret-free reconstruction plan)

The non-secret re-provision plan — guest sizing, drive inventory (durable-id → role → mount → intent), PVE storage defs, PBS coordinates, and app inventory + storage bindings. It complements escrow (keys) and PBS/restic (bytes): it contains no key, password, or token. Use it to rebuild the host/guest/storage scaffolding before the PBS bytes land.

Storage / guest / PBS half (agent) {{if .DRRecipeHasHost}}present{{else}}awaiting host-report{{end}}
Customer / apps half (controller) {{if .DRRecipeHasApps}}present{{else}}awaiting controller report{{end}}
Last updated {{if .DRRecipeUpdatedAt}}{{.DRRecipeUpdatedAt}}{{else}}—{{end}}
Download recipe (JSON)
{{else}}

No DR recipe yet — it assembles from the host-report and controller report.

{{end}} {{else}}

Backup and DR data appear once the first report arrives.

{{end}}
{{if .HasReports}}

Events {{if .EventCounts}} {{with mapGet .EventCounts "critical"}}{{.}} critical{{end}} {{with mapGet .EventCounts "error"}}{{.}} error{{if gt . 1}}s{{end}}{{end}} {{with mapGet .EventCounts "warning"}}{{.}} warning{{if gt . 1}}s{{end}}{{end}} {{end}} (last 24h)

{{if .Events}}
{{range .Events}} {{end}}
Time Severity Type Message Source
{{.CreatedAt.Format "Jan 02 15:04"}} {{.Severity}} {{.EventType}} {{.Message}} {{.Source}}
{{else}}

No events recorded yet.

{{end}}
{{if .History}}

Report History (last 24h)

{{len .History}} reports {{range .History}} {{end}}
Time Status CPU Memory
{{.ReceivedAt.Format "Jan 02 15:04"}} {{.HealthStatus}} {{formatFloat .CPUPercent}}% {{formatFloat .MemoryPercent}}%
{{end}} {{else}}

Events appear once the first report arrives.

{{end}}
{{if .HasReports}}

Notifications

Email {{if .NotifPrefs}}{{if .NotifPrefs.Email}}{{.NotifPrefs.Email}}{{else}}Not set{{end}}{{else}}Not configured{{end}}
{{if .NotifPrefs}}
Events {{if .NotifPrefs.EnabledEvents}}{{joinStrings .NotifPrefs.EnabledEvents ", "}}{{else}}None{{end}}
{{end}}
{{if .RecentNotifications}}

Recent (last 10)

{{range .RecentNotifications}} {{end}}
Time Channel Event Status Message
{{.CreatedAt.Format "Jan 02 15:04"}} {{.Channel}} {{.EventType}} {{.Status}} {{.Message}}
{{end}}
{{else}}

Notification data appears once the first report arrives.

{{end}}
{{if .Hosts}} {{range .Hosts}}

Open host page: {{.HostID}} →

{{template "host_detail_body" .}} {{end}} {{else}}

No host enrolled yet.

A host appears here once it enrolls via the Day-0 bootstrap.

{{end}}
{{if .HasReports}} {{end}}