{{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 "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). {{end}}
{{end}} {{if .IsBlocked}}
This customer is blocked — reports are accepted but not shown on the Dashboard.
{{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}}

Setup Command

Day-0 host bootstrap for host-install {{.ScriptVersion}}. Run 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}}

Danger zone

Blocking hides the customer from the Dashboard (reports are still accepted); deleting removes the managed configuration permanently.

{{if .IsBlocked}}
{{.CSRFField}}
{{else}}
{{.CSRFField}}
{{end}}
{{.CSRFField}}
{{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}}