Files
felhom-controller/controller/internal/web/templates/dashboard.html
T
admin e66d4669d6
gates / gates (push) Failing after 47s
v0.315.0: one app page with four tabs; the guide the catalog can carry; no Raspberry Pi badge
An app was six pages — details, install/settings, logs, export, the per-app backup panel and a
details card for moving data — and every "Vissza" on them went to the app LIST, so there was no way
back to the app you were looking at. It is now ONE page with four tab URLs under /apps/{slug}/,
one shared header and one set of banners.

  settings  Mukodes (Start/Restart/Stop, Stop confirms) + the cards that want something from the
            household + the install-time and optional settings. The default for an installed app,
            RENDERED at the bare URL and never redirected to.
  data      the data folders, move to another drive, the nightly copy, the 2nd-copy setting, export
  guide     tagline, screenshots, use cases, first steps, the catalog's guide, the FAQ, the docs
            link — and the WHOLE page for an app that is not installed
  logs      the log, and the only tab whose poller runs

The tabs are links, not panels: a link in an alert has to land ON a tab, and the log poller must not
run on the other three. Start/Restart/Stop left the page header, where they read as menu buttons.

The old /stacks/{name}/{deploy,logs,export,backup} GETs answer 302 — never 301, which a browser
caches beyond anyone's reach — to the tab that now holds them, query string carried through, with
name->slug resolved through the stack list rather than assumed equal. Every POST on those paths is
untouched: the backup form, the app-email toggle and the reveal endpoints keep their URLs.

A value asked AFTER the app starts is now generic (D2). The catalog marks a field `after_start: true`
and supplies its label; the ACTION is Go, in a registry keyed by slug + env var. The catalog can
never declare what runs — a declared field with no registered action renders nothing, POSTs 404 and
logs one WARN with the env var KEY, never the value. A red-proof found the second half of that guard
unpinned: deleting the "the catalog must have DECLARED this field" check left the suite green, and a
POST for an undeclared field then reached Plex and opened the setup gate.
TestAfterStart_RegistryAloneIsNotAnInvitation pins it.

New catalog block app_info.guide (sections + faq + checked_version), parsed, localised field by
field and rendered with the household's own address filled in. It renders only when checked_version
names the image tag its author clicked through. No template carries one yet.

The Pi and "csak x86" badges are gone; PiCompatible and the catalog key stay this release, or a box
on 0.314.0 would read "csak x86" on every app (R-1067).

template_id_gate.py now follows {{template}} includes, with a scope decoy proving the walk did not
become vacuous.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
2026-10-15 18:37:50 +02:00

235 lines
14 KiB
HTML

{{define "dashboard"}}
{{template "layout_start" .}}
<div class="page-header">
<h2>{{T "dashboard.vezerlopult"}}</h2>
<span class="domain-badge">{{.Domain}}</span>
</div>
{{template "media_notice" .}}
<div class="stats-grid">
<a href="/stacks?filter=running" class="stat-card">
<div class="stat-value">{{.RunningCount}}</div>
<div class="stat-label">{{T "dashboard.futo_alkalmazas"}}</div>
</a>
<a href="/stacks?filter=stopped" class="stat-card {{if gt .StoppedCount 0}}stat-warn{{else}}stat-zero{{end}}">
<div class="stat-value">{{.StoppedCount}}</div>
<div class="stat-label">{{T "dashboard.leallitva"}}</div>
</a>
<a href="/stacks" class="stat-card">
<div class="stat-value">{{.TotalCount}}</div>
<div class="stat-label">{{T "dashboard.osszes_alkalmazas"}}</div>
</a>
</div>
{{if .SystemInfo.TotalMemMB}}
<div class="system-info-card">
<div class="system-info-items">
<div class="system-info-item meter {{usageColor .SystemInfo.MemPercent}}">
<div class="system-info-header">
<span class="system-info-label">{{T "dashboard.memoria"}}</span>
<span class="system-info-value">{{fmtMB .SystemInfo.UsedMemMB}} / {{fmtMB .SystemInfo.TotalMemMB}} ({{printf "%.0f" .SystemInfo.MemPercent}}%)</span>
</div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.0f" .SystemInfo.MemPercent}}%"></div>
</div>
</div>
<div class="system-info-item meter {{usageColor .SystemInfo.CPUPercent}}">
<div class="system-info-header">
<span class="system-info-label">CPU</span>
<span class="system-info-value">{{printf "%.0f" .SystemInfo.CPUPercent}}%</span>
</div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.0f" .SystemInfo.CPUPercent}}%"></div>
</div>
<div class="system-load-avg">Load: {{fmtLoad .SystemInfo.LoadAvg1}} / {{fmtLoad .SystemInfo.LoadAvg5}} / {{fmtLoad .SystemInfo.LoadAvg15}}</div>
</div>
{{if .SystemInfo.TemperatureCelsius}}
<div class="system-info-item system-info-item-compact">
<div class="system-info-header">
<span class="system-info-label"><svg class="ico ico-sm"><use href="#i-thermometer"/></svg> {{T "dashboard.homerseklet"}}</span>
<span class="system-info-value temp-value state-text-{{tempColor .SystemInfo.TemperatureCelsius}}">{{fmtTemp .SystemInfo.TemperatureCelsius}}</span>
</div>
</div>
{{end}}
</div>
<div class="system-info-items" style="margin-top: 1rem;">
{{/* R-259: the figures and the bar render ONLY when the measurement succeeded. A statfs
failure used to leave every number at zero, and usageColor(0) is "nominal" — so a disk
we could not read was drawn as a healthy empty one. Same reasoning as the off-site
card: a 0%-wide bar over an unmeasured store is a picture of emptiness, and a picture
is a claim. */}}
{{if not .SystemInfo.DiskKnown}}
<div class="system-info-item meter meter-empty">
<div class="system-info-header">
<span class="system-info-label">{{T "common.rendszer_gyoker"}}</span>
<span class="system-info-value">— <span class="state-text-neutral">{{T "dashboard.nem_ismert"}}</span></span>
</div>
<div class="meter-track"></div>
<div class="meter-flag">{{T "dashboard.a_tarhely_merete_most_nem"}}</div>
</div>
{{else}}
{{$duc := usageColor .SystemInfo.DiskPercent}}
<div class="system-info-item meter {{$duc}}">
<div class="system-info-header">
<span class="system-info-label">{{T "common.rendszer_gyoker"}}</span>
<span class="system-info-value">{{fmtGB .SystemInfo.DiskUsedGB}} / {{fmtGB .SystemInfo.DiskTotalGB}} ({{printf "%.0f" .SystemInfo.DiskPercent}}%)</span>
</div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.0f" .SystemInfo.DiskPercent}}%"></div>
</div>
{{if eq $duc "warn"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>{{T "dashboard.fogyoban_a_hely"}}</div>{{else if eq $duc "crit"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>{{T "dashboard.kritikusan_keves_hely"}}</div>{{end}}
</div>
{{end}}
{{range .StorageBars}}
{{if .Disconnected}}
<div class="system-info-item meter meter-empty storage-disconnected">
<div class="system-info-header">
<span class="system-info-label">{{.Label}}</span>
<span class="system-info-value">— <span class="state-text-neutral">{{T "common.levalasztva"}}</span></span>
</div>
<div class="meter-track"></div>
</div>
{{else}}
{{$uc := usageColor .Percent}}
<div class="system-info-item meter {{$uc}}">
<div class="system-info-header">
<span class="system-info-label">{{.Label}}</span>
<span class="system-info-value">{{fmtGB .UsedGB}} / {{fmtGB .TotalGB}} ({{printf "%.0f" .Percent}}%)</span>
</div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.0f" .Percent}}%"></div>
</div>
{{if eq $uc "warn"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>{{T "dashboard.fogyoban_a_hely"}}</div>{{else if eq $uc "crit"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>{{T "dashboard.kritikusan_keves_hely"}}</div>{{end}}
</div>
{{end}}
{{end}}
</div>
{{if .SettingsWarning}}
<div class="inline-warnings">
<div class="inline-warning inline-warning-error">
<span class="inline-warning-dot">●</span>
<span class="inline-warning-text">{{.SettingsWarning}}</span>
</div>
</div>
{{end}}
{{if .DiskWarnings}}
<div class="inline-warnings">
{{range .DiskWarnings}}
<div class="inline-warning inline-warning-{{.Level}}">
<span class="inline-warning-dot">●</span>
<span class="inline-warning-text">{{.Message}}</span>
{{if .Link}}<a href="{{.Link}}" class="inline-warning-link">{{.LinkText}} →</a>{{end}}
</div>
{{end}}
</div>
{{end}}
</div>
{{end}}
<div class="disk-health-card">
<h3>{{T "dashboard.lemezek_allapota"}}</h3>
{{if .DiskHealthRows}}
<ul class="disk-health-list">
{{range .DiskHealthRows}}
<li class="disk-health-row">
<span class="disk-health-label">{{.Label}}</span>
<span class="disk-health-status {{.ChipClass}}">{{.ChipLabel}}</span>
{{if .Temp}}<span class="disk-health-temp">{{.Temp}} °C</span>{{end}}
</li>
{{end}}
</ul>
{{else}}
<p class="form-hint state-text-neutral">{{T "dashboard.nincs_adat"}}</p>
{{end}}
</div>
{{if .BackupEnabled}}
<div class="backup-status-card">
<h3><a href="/backups" class="backup-card-link">{{T "dashboard.biztonsagi_mentes"}}</a></h3>
{{if .BackupStatus}}
<div class="backup-info-row">
<span class="backup-label">{{T "dashboard.utolso_mentes"}}</span>
<span class="backup-value">
{{if .BackupStatus.Success}}
<span>{{fmtTime .BackupStatus.LastRun}}</span>
{{else}}
<span class="backup-status-fail">{{T "dashboard.sikertelen"}}</span>
{{end}}
</span>
</div>
{{else}}
<div class="backup-info-row">
<span class="backup-label">{{T "dashboard.utolso_mentes"}}</span>
<span class="backup-value backup-status-none">{{T "dashboard.meg_nem_futott"}}</span>
</div>
{{end}}
{{if .DBDumpStatus}}
<div class="backup-info-row">
<span class="backup-label">{{T "dashboard.adatbazisok"}}</span>
<span class="backup-value">{{T "dashboard.mentve"}}</span>
</div>
{{end}}
{{/* Cross-drive ("2. mentés") moved to the host agent in slice 8C — the dashboard handler no
longer provides CrossDrive* fields, so the old block was a dead reference that 500'd the
dashboard (gt <nil> 0). Removed in v0.41.2. */}}
</div>
{{end}}
<h3>{{T "dashboard.telepitett_alkalmazasok"}}</h3>
<div class="stack-list">
{{range .Stacks}}
{{$im := infraMeta .Name}}
{{if $im}}
{{template "app_list_row" dict "Slug" .Meta.Slug "Name" $im.DisplayName "Secondary" $im.Description "RowClass" (printf "stack-state-%s" (stateColor .State)) "Href" (appHref .Meta.Slug) "FallbackIcon" "/static/infra-logo.svg"}}
{{else}}
{{template "app_list_row" dict "Slug" .Meta.Slug "Name" .Meta.DisplayName "Secondary" .Meta.Description "RowClass" (printf "stack-state-%s" (stateColor .State)) "Href" (appHref .Meta.Slug)}}
{{end}}
<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>
{{if .Orphaned}}<span class="tag tag-warn">{{T "common.elavult"}}</span>{{end}}
{{template "meta_badge" (lifecycleBadge .Meta)}}
{{$ms := index $.MissingStorage .Name}}{{if $ms}}<span class="tag tag-warn" title="{{T "dashboard.az_alkalmazas_adattaroloja_nem_elerheto"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "dashboard.hianyzo_tarhely"}}</span>{{end}}
{{if $.OOMKilled}}{{if index $.OOMKilled .Name}}<span class="tag tag-warn" title="{{T "dashboard.az_alkalmazas_egyik_folyamatat_a"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "dashboard.memoria_elfogyott"}}</span>{{end}}{{end}}
{{$ns := index $.NetworkStubs .Name}}{{if $ns}}<span class="tag tag-warn" title="{{T "dashboard.az_alkalmazas_kornyezeteben_a_halozati"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "dashboard.halozati_tarhely_hibas_az_alkalmazas"}}</span>{{end}}
{{$nw := index $.NetworkWarnings .Name}}{{if $nw}}<span class="tag tag-warn" title="{{T "dashboard.a_halozati_tarhely_nas_jelenleg"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "dashboard.halozati_tarhely_nem_elerheto"}}</span>{{end}}
{{if and .Deployed (routeUnpublished .State)}}<span class="tag tag-warn" title="{{T "dashboard.a_proxy_traefik_csak_egeszseges"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "dashboard.url_nem_elerheto"}}</span>{{end}}{{if and (eq .Name "cloudflared") (eq (printf "%s" .State) "unhealthy")}}<span class="tag tag-warn" data-tunnel-offline="true" title="{{T "dashboard.tunnel_offline_title"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "dashboard.tunnel_offline"}}</span>{{end}}
{{if .Protected}}
<span class="tag"><svg class="ico ico-sm"><use href="#i-lock"/></svg>{{T "dashboard.vedett"}}</span>
{{/* Customer link ONLY for Linked infra (filebrowser → files.<domain>) —
cloudflared/traefik have no customer UI (guarded by render test). */}}
{{if and $im $im.Linked}}
{{$subdomain := index $.Subdomains .Name}}
{{if $subdomain}}<a href="https://{{$subdomain}}.{{$.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline" onclick="event.stopPropagation()">{{T "dashboard.megnyitas"}} <svg class="ico ico-sm"><use href="#i-external-link"/></svg></a>{{end}}
{{end}}
{{if isOperational .State}}
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')" title="{{T "common.ujrainditas"}}"><svg class="ico ico-sm"><use href="#i-rotate-cw"/></svg></button>
{{end}}
{{else if not .Deployed}}
<a href="{{appPageURL .Meta.Slug}}/install" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, '{{.Name}}')">{{T "dashboard.telepites"}}</a>
{{else}}
{{if isOperational .State}}
{{$subdomain := index $.Subdomains .Name}}
{{if $subdomain}}<a href="https://{{$subdomain}}.{{$.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline" onclick="event.stopPropagation()">{{T "dashboard.megnyitas"}} <svg class="ico ico-sm"><use href="#i-external-link"/></svg></a>{{end}}
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')" title="{{T "common.ujrainditas"}}"><svg class="ico ico-sm"><use href="#i-rotate-cw"/></svg></button>
<button class="btn btn-sm btn-danger" onclick="stackAction(event, '{{.Name}}', 'stop')" title="{{T "dashboard.leallitas"}}"><svg class="ico ico-sm"><use href="#i-square"/></svg></button>
{{else}}
<button class="btn btn-sm btn-success" onclick="stackAction(event, '{{.Name}}', 'start')" title="{{T "dashboard.inditas"}}"><svg class="ico ico-sm"><use href="#i-play"/></svg></button>
{{if not .Orphaned}}<button class="btn btn-sm btn-danger" onclick="removeStack('{{.Name}}')">{{T "common.eltavolitas"}}</button>{{end}}
{{end}}
<a href="{{appPageURL .Meta.Slug}}/logs" class="btn btn-sm btn-outline">{{T "dashboard.naplo"}}</a>
{{if .Orphaned}}<button class="btn btn-sm btn-danger" onclick="deleteOrphanStack('{{.Name}}')">{{T "common.torles"}}</button>{{end}}
{{end}}
{{template "app_list_row_end"}}
{{else}}
<div class="empty-state">
<p>{{T "dashboard.nincs_elerheto_alkalmazas"}}</p>
</div>
{{end}}
</div>
{{template "layout_end" .}}
{{end}}