e66d4669d6
gates / gates (push) Failing after 47s
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
437 lines
26 KiB
HTML
437 lines
26 KiB
HTML
{{define "backups_apps"}}
|
||
{{template "layout_start" .}}
|
||
|
||
<div class="page-header">
|
||
<h2>{{T "backups_apps.biztonsagi_mentes_alkalmazasok"}}</h2>
|
||
<span class="domain-badge">{{.Domain}}</span>
|
||
</div>
|
||
|
||
{{template "backups_flash" .}}
|
||
{{template "restore_banner" .}}{{if .Backup}}{{if .Backup.Interrupted}}
|
||
<div class="alert alert-warning" data-interrupted-run="true">{{T "backups.interrupted_run"}}</div>
|
||
{{end}}{{end}}
|
||
{{- if .HollowCopyLines}}
|
||
<div class="alert alert-error" style="margin-bottom:1.5rem">
|
||
{{range .HollowCopyLines}}<div>{{.}}</div>{{end}}
|
||
</div>
|
||
{{- end}}
|
||
|
||
{{if not .Backup}}
|
||
{{template "backups_empty" .}}
|
||
{{else}}
|
||
|
||
{{if .OffboxOfferCount}}
|
||
<div class="alert alert-info" style="margin-top:.75rem" data-offsite-offer="true">
|
||
<span>{{T "backups_offsite.offer_text"}} ({{.OffboxOfferCount}})</span>
|
||
<form method="POST" action="/backup/offbox/enable-all" style="display:inline;margin-left:.5rem">{{$.CSRFField}}<button type="submit" class="btn btn-xs btn-primary">{{T "backups_offsite.offer_all"}}</button></form>
|
||
<form method="POST" action="/backup/offbox/offer-dismiss" style="display:inline">{{$.CSRFField}}<button type="submit" class="btn btn-xs btn-outline">{{T "backups_offsite.offer_no"}}</button></form>
|
||
</div>
|
||
{{end}}<h3 class="backup-tier-divider">{{T "backups_apps.alkalmazas_mentesek_adatbazis_konfigurac"}}</h3>
|
||
<p class="form-hint" style="margin:-0.25rem 0 1rem">{{T "backups_apps.az_egyes_alkalmazasok_reszletes_granulal"}}</p>
|
||
|
||
<!-- Section 2: Schedule -->
|
||
<div class="schedule-card">
|
||
<h3>{{T "backups_apps.utemezes"}}</h3>
|
||
<div class="schedule-rows">
|
||
<div class="schedule-row">
|
||
<span class="schedule-task">{{T "backups_apps.adatbazis_mentes"}}</span>
|
||
<span class="schedule-time">{{.Backup.DBDumpSchedule}}</span>
|
||
<span class="schedule-next">{{T "backups_apps.kovetkezo"}}</span>
|
||
</div>
|
||
</div>
|
||
<div class="schedule-summary">
|
||
{{if .Backup.LastDBDump}}
|
||
<div class="schedule-summary-row">
|
||
<span>{{T "backups_apps.utolso_adatbazis_mentes"}}</span>
|
||
<span class="schedule-summary-value">{{fmtTime .Backup.LastDBDump.LastRun}} ({{timeAgo .Backup.LastDBDump.LastRun}})</span>
|
||
</div>
|
||
{{else}}
|
||
<div class="schedule-summary-row">
|
||
<span>{{T "backups_apps.utolso_adatbazis_mentes"}}</span>
|
||
<span class="schedule-summary-value relative-time">{{T "backups_apps.meg_nem_futott"}}</span>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
<div class="schedule-actions">
|
||
<button class="btn btn-sm btn-primary" onclick="triggerBackupFromPage()" id="backup-page-btn"
|
||
{{if .Backup.Running}}disabled{{end}}>
|
||
{{if .Backup.Running}}{{T "backups_apps.mentes_folyamatban"}}{{else}}{{T "backups_apps.mentes_most"}}{{end}}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 3: Databases -->
|
||
<div class="backup-section-card">
|
||
<h3>{{T "backups_apps.adatbazisok"}}</h3>
|
||
{{if or .Backup.DumpFiles .Backup.DiscoveredDBs}}
|
||
<div class="backup-table-wrap">
|
||
<table class="db-table">
|
||
<thead>
|
||
<tr>
|
||
<th>{{T "backups_apps.alkalmazas"}}</th>
|
||
<th>{{T "backups_apps.tipus"}}</th>
|
||
<th>{{T "backups_apps.meret"}}</th>
|
||
<th>{{T "backups_apps.utolso"}}</th>
|
||
<th>{{T "backups_apps.ervenyesites"}}</th>
|
||
<th>{{T "backups_apps.allapot"}}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{{if .Backup.LastDBDump}}
|
||
{{range .Backup.LastDBDump.Results}}
|
||
<tr>
|
||
<td>{{.DB.StackName}}</td>
|
||
<td><span class="db-type-badge db-type-{{.DB.DBType}}">{{dbTypeLabel .DB.DBType}}</span></td>
|
||
<td class="mono">{{if .Error}}–{{else}}{{fmtBytes .Size}}{{end}}</td>
|
||
<td class="mono">{{if .Error}}–{{else}}{{fmtTimeShort $.Backup.LastDBDump.LastRun}}{{end}}</td>
|
||
<td>
|
||
{{if .Error}}
|
||
<span class="validation-badge validation-na">–</span>
|
||
{{else if .Validation.Valid}}
|
||
<span class="validation-badge validation-ok">{{T "backups_apps.tabla"}}</span>
|
||
{{else if .Validation.Error}}
|
||
<span class="validation-badge validation-fail" title="{{.Validation.Error}}">{{T "backups_apps.hiba"}}</span>
|
||
{{else}}
|
||
<span class="validation-badge validation-na" title="{{T "backups_apps.az_ervenyesites_nem_futott_le"}}">–</span>
|
||
{{end}}
|
||
</td>
|
||
<td>
|
||
{{if .Error}}
|
||
<span class="validation-badge validation-fail" title="{{.Error}}">{{T "backups_apps.hiba"}}</span>
|
||
{{else}}
|
||
<span class="validation-badge validation-ok">OK</span>
|
||
{{end}}
|
||
</td>
|
||
</tr>
|
||
{{end}}
|
||
{{else}}
|
||
{{range .Backup.DumpFiles}}
|
||
<tr>
|
||
<td>{{.StackName}}</td>
|
||
<td><span class="db-type-badge db-type-{{.DBType}}">{{dbTypeLabel .DBType}}</span></td>
|
||
<td class="mono">{{fmtBytes .Size}}</td>
|
||
<td class="mono">{{fmtTimeShort .ModTime}}</td>
|
||
<td>
|
||
{{if .Validation.Valid}}
|
||
<span class="validation-badge validation-ok">{{T "backups_apps.tabla"}}</span>
|
||
{{else if .Validation.Error}}
|
||
<span class="validation-badge validation-fail" title="{{.Validation.Error}}">{{T "backups_apps.hiba"}}</span>
|
||
{{else}}
|
||
<span class="validation-badge validation-na">–</span>
|
||
{{end}}
|
||
</td>
|
||
<td><span class="validation-badge validation-ok">OK</span></td>
|
||
</tr>
|
||
{{end}}
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{{else if eq .DBSectionState "pending"}}
|
||
<div class="backup-table-empty">{{T "backups_apps.meg_nem_keszult_adatbazis_mentes"}}</div>
|
||
{{else}}
|
||
<div class="backup-table-empty">{{T "backups_apps.a_telepitett_alkalmazasok_beagyazott_ada"}}</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- Section 4: Unified per-app backup status -->
|
||
{{if .Backup.AppDataInfo}}
|
||
<div class="backup-section-card">
|
||
<h3>{{T "backups_apps.alkalmazasok_mentesi_allapota"}}</h3>
|
||
|
||
{{if .NoUserDataBackupWarning}}
|
||
<div class="alert alert-error" style="margin-bottom:1.5rem">
|
||
{{T "backups_apps.felhasznaloi_adatokrol_nincs_biztonsagi"}}
|
||
</div>
|
||
{{end}}
|
||
|
||
{{range .AppBackupRows}}
|
||
<div class="app-backup-row" data-status="{{.Status}}">
|
||
<!-- Aligned to the shared app-row grammar (icon + name left, status + chevron right);
|
||
the expander behavior is untouched — this header is the one allowlisted aligned
|
||
copy in scripts/app_row_dedup_gate.py, NOT a partial render (it owns the toggle).
|
||
The icon chain matches app_row.html: SVG -> PNG -> /static/app-placeholder.svg, so a
|
||
logo-less app shows the placeholder here too instead of a hidden dead-end (v0.163.1). -->
|
||
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
|
||
<img class="app-row-icon" src="{{logoURL .Slug}}" alt="" data-fallback="/static/app-placeholder.svg"
|
||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Slug}}';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}">
|
||
<span class="app-backup-row-name">{{.DisplayName}}</span>
|
||
<div class="app-backup-row-meta">
|
||
{{if .Removed}}
|
||
<span class="tag tag-warn">{{T "backups_apps.eltavolitva_visszaallithato"}}</span>
|
||
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
|
||
{{else if .DriveDisconnected}}
|
||
<span class="tag tag-warn">{{T "backups_apps.meghajto_levalasztva"}}</span>
|
||
{{else if .HasHDDData}}
|
||
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
|
||
<span class="mono app-backup-size" style="font-size:.8rem">{{numText .HDDSizeHuman}}</span>
|
||
{{else if .HasVolumeData}}
|
||
<span class="meta-badge">{{T "backups_apps.konfig"}}{{if .HasDB}} + DB{{end}} {{T "backups_apps.adatok"}}</span>
|
||
{{else}}
|
||
<span class="meta-badge">{{T "backups_apps.konfig"}}{{if .HasDB}} + DB{{end}}</span>
|
||
{{end}}
|
||
<span class="status-dot status-{{.Status}}" title="{{.StatusText}}"></span>
|
||
</div>
|
||
<span class="expand-icon">▶</span>
|
||
</div>
|
||
<div class="app-backup-row-detail" style="display:none">
|
||
{{if .Removed}}
|
||
{{/* R-487: a removed app's kept unit. One action — the same restore the picker on the
|
||
Visszaállítás page runs (POST /backup/restore, snapshot „helyi"); it reinstalls the app
|
||
from the unit (R-253). No tier rows: nothing is scheduled for an app that is not here. */}}
|
||
<div class="backup-layers">
|
||
<div class="backup-layer-row">
|
||
<span class="tier-label">{{T "backups_apps.mentes"}}</span>
|
||
<span class="layer-badge">{{T "backups_apps.megtartva"}}</span>
|
||
<span class="tier-location">{{T "backups_apps.helyi"}}</span>
|
||
<span class="tier-contents">{{T "backups_apps.az_alkalmazast_eltavolitottad_a_mentese"}}</span>
|
||
<div class="layer-actions">
|
||
<form method="POST" action="/backup/restore" style="display:inline">{{$.CSRFField}}
|
||
<input type="hidden" name="stack_name" value="{{.StackName}}">
|
||
<input type="hidden" name="snapshot_id" value="helyi">
|
||
<button type="submit" class="btn btn-xs btn-outline" data-confirm="{{T "backups_apps.ujratelepiti_a_z_alkalmazast_a"}}">{{T "backups_apps.visszaallitas_a_mentesbol"}}</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{{else}}
|
||
<div class="backup-layers">
|
||
<!-- Tier 1: Nightly backup (mandatory, same drive) -->
|
||
<div class="backup-layer-row">
|
||
<span class="tier-label">{{T "backups_apps.1_mentes"}}</span>
|
||
<span class="layer-badge">Auto</span>
|
||
<span class="tier-location">{{T "backups_apps.helyi"}}</span>
|
||
{{if .Tier1LastRun}}
|
||
{{/* R-258: three states. The tick is THIS app's own most recent dump result,
|
||
not the box's most recent run — and a third value renders NO icon, which is
|
||
where "we have no result for this backup" belongs. A tick standing for
|
||
"a file exists" is what this replaced. */}}
|
||
<span class="layer-last">{{T "backups_apps.utolso_2"}}
|
||
{{if eq .Tier1LastStatus "ok"}}<span class="text-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg></span>
|
||
{{else if eq .Tier1LastStatus "error"}}<span class="text-error"><svg class="ico ico-sm"><use href="#i-x"/></svg></span>
|
||
{{else}}<span class="state-text-neutral" title="{{T "backups_apps.errol_a_mentesrol_nincs_eredmenyunk"}}">—</span>{{end}}
|
||
</span>
|
||
{{end}}
|
||
<span class="tier-contents">{{.Tier1Contents}}</span>
|
||
{{if .DriveFilesNote}}<span class="state-text-neutral" style="font-size:.8rem">{{.DriveFilesNote}}{{if .DriveFilesNoteLink}} <a href="{{.DriveFilesNoteLink}}">{{.DriveFilesNoteLinkText}} →</a>{{end}}</span>{{end}}
|
||
{{if and .HasDB (eq .Tier1DBStatus "error")}}
|
||
<span class="text-error" style="font-size:.8rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "backups_apps.db_dump_hiba"}}</span>
|
||
{{end}}
|
||
</div>
|
||
<!-- Tier 2: Cross-drive backup (opt-in, different device) -->
|
||
<div class="backup-layer-row">
|
||
<span class="tier-label">{{T "backups_apps.2_mentes"}}</span>
|
||
{{if .Tier2UserDisabled}}
|
||
<span class="layer-unconfigured">{{T "backups_apps.2_mentes_kikapcsolva"}}</span>
|
||
<div class="layer-actions">
|
||
<a href="{{appPageURL .Slug}}/data" class="btn btn-xs btn-outline">{{T "backups_apps.beallitas"}}</a>
|
||
</div>
|
||
{{else if and .Tier2Configured .Tier2DestDisconnected}}
|
||
<span class="layer-method" style="opacity:.6">rsync</span>
|
||
<span class="layer-dest" style="opacity:.6">→ {{.Tier2Dest}}</span>
|
||
<span class="tag tag-warn">{{T "backups_apps.cel_meghajto_levalasztva"}}</span>
|
||
{{if not .Tier2SuccessTracked}}{{if .Tier2LastRun}}
|
||
<span class="layer-last" style="opacity:.6">{{T "backups_apps.utolso_3"}}</span>
|
||
{{end}}{{else if .Tier2LastSuccess}}
|
||
<span class="layer-last" style="opacity:.6">{{T "backups_apps.utolso_sikeres"}}</span>
|
||
{{else}}
|
||
<span class="layer-last" style="opacity:.6">{{T "backups_apps.meg_nincs_sikeres_masolat"}}</span>
|
||
{{end}}
|
||
<span class="tier-contents" style="opacity:.6">{{.Tier23Contents}}</span>
|
||
<div class="layer-actions">
|
||
<a href="{{appPageURL .Slug}}/data" class="btn btn-xs btn-outline">{{T "backups_apps.beallitas"}}</a>
|
||
</div>
|
||
{{else if and .Tier2Configured .Tier2DestInactive}}
|
||
<span class="layer-method" style="opacity:.6">rsync</span>
|
||
<span class="layer-dest" style="opacity:.6">→ {{.Tier2Dest}}</span>
|
||
<span class="tag tag-warn">{{T "backups_apps.cel_meghajto_inaktiv"}}</span>
|
||
{{if not .Tier2SuccessTracked}}{{if .Tier2LastRun}}
|
||
<span class="layer-last" style="opacity:.6">{{T "backups_apps.utolso_3"}}</span>
|
||
{{end}}{{else if .Tier2LastSuccess}}
|
||
<span class="layer-last" style="opacity:.6">{{T "backups_apps.utolso_sikeres"}}</span>
|
||
{{else}}
|
||
<span class="layer-last" style="opacity:.6">{{T "backups_apps.meg_nincs_sikeres_masolat"}}</span>
|
||
{{end}}
|
||
<span class="tier-contents" style="opacity:.6">{{.Tier23Contents}}</span>
|
||
<div class="layer-actions">
|
||
<a href="{{appPageURL .Slug}}/data" class="btn btn-xs btn-outline">{{T "backups_apps.beallitas"}}</a>
|
||
</div>
|
||
{{else if .Tier2Configured}}
|
||
<span class="layer-method">rsync</span>
|
||
<span class="layer-dest">→ {{.Tier2Dest}}</span>
|
||
<span class="layer-schedule">{{.Tier2Schedule}}</span>
|
||
{{if not .Tier2SuccessTracked}}{{if .Tier2LastRun}}
|
||
<span class="layer-last">{{T "backups_apps.utolso_3"}}
|
||
<span class="{{if eq .Tier2LastStatus "ok"}}text-ok{{else if eq .Tier2LastStatus "error"}}text-error{{else if eq .Tier2LastStatus "running"}}text-muted{{end}}">
|
||
{{.Tier2StatusBadge}}
|
||
</span>
|
||
</span>
|
||
{{end}}{{else if .Tier2LastSuccess}}
|
||
<span class="layer-last">{{T "backups_apps.utolso_sikeres"}}
|
||
<span class="{{if eq .Tier2LastStatus "ok"}}text-ok{{else if eq .Tier2LastStatus "error"}}text-error{{else if eq .Tier2LastStatus "running"}}text-muted{{end}}">
|
||
{{.Tier2StatusBadge}}
|
||
</span>
|
||
</span>
|
||
{{else}}
|
||
<span class="layer-last">{{T "backups_apps.meg_nincs_sikeres_masolat"}}
|
||
<span class="{{if eq .Tier2LastStatus "error"}}text-error{{else}}text-muted{{end}}">{{.Tier2StatusBadge}}</span>
|
||
</span>
|
||
{{end}}
|
||
{{if .Tier2SizeHuman}}<span class="tier-size">{{numText .Tier2SizeHuman}}</span>{{end}}
|
||
{{if .Tier2LastWarning}}<span class="layer-reason" style="color:var(--warn);opacity:.9">{{.Tier2LastWarning}}</span>{{end}}
|
||
{{/* R-403: a run that PRESERVED the copy's package instead of refreshing it must
|
||
not render as a plain fresh copy. The status line above is about the RUN;
|
||
this one is about the PACKAGE, and after a preserved leg they differ. */}}
|
||
{{if .Tier2UnitStaleNotice}}<span class="layer-reason" style="color:var(--warn);opacity:.9">{{.Tier2UnitStaleNotice}}</span>{{end}}
|
||
<span class="tier-contents">{{.Tier23Contents}}</span>
|
||
<span class="tier-browsable" title="{{T "backups_apps.a_mentes_bongeszheto_fajlrendszerben"}}"><svg class="ico ico-sm"><use href="#i-file-text"/></svg></span>
|
||
<div class="layer-actions">
|
||
{{if not .Tier2SuccessTracked}}{{if .Tier2LastRun}}
|
||
<form method="POST" action="/backup/tier2/restore" style="display:inline">{{$.CSRFField}}
|
||
<input type="hidden" name="stack_name" value="{{.StackName}}">
|
||
<button type="submit" class="btn btn-xs btn-outline" data-confirm="{{T "backups_apps.visszaallitja_a_hianyzo_fajlokat_a"}}">{{T "backups_apps.fajlok_visszaallitasa"}}</button>
|
||
</form>
|
||
{{end}}{{else if .Tier2LastSuccess}}
|
||
<form method="POST" action="/backup/tier2/restore" style="display:inline">{{$.CSRFField}}
|
||
<input type="hidden" name="stack_name" value="{{.StackName}}">
|
||
<button type="submit" class="btn btn-xs btn-outline" data-confirm="{{T "backups_apps.visszaallitja_legutobbi_sikeres_masolat"}}{{if .Tier2StaleCopy}}{{T "backups_apps.figyelem_legutobbi_kiserlet_nem_sikerult"}}{{end}}">{{T "backups_apps.fajlok_visszaallitasa"}}</button>
|
||
</form>
|
||
{{else}}
|
||
<span class="layer-reason" style="opacity:.85">{{T "backups_apps.meg_nincs_sikeres_masolat_amibol"}}</span>
|
||
{{end}}
|
||
{{/* R-102/R-103: the DESTRUCTIVE twin of the button above. Offered only when the
|
||
copy holds an openable recovery unit (Tier2UnitRestorable — a second
|
||
predicate, never a widening of the file restore's). It is deliberately a
|
||
SEPARATE button in a danger style: the one beside it only adds what is
|
||
missing, this one overwrites the app's live data with the copy's. */}}
|
||
{{if .Tier2UnitRestorable}}
|
||
<form method="POST" action="/backup/tier2/unit-restore" style="display:inline">{{$.CSRFField}}
|
||
<input type="hidden" name="stack_name" value="{{.StackName}}">
|
||
<button type="submit" class="btn btn-xs btn-danger-outline" data-confirm="{{.Tier2UnitConfirm}}">{{T "backups_apps.teljes_visszaallitas_a_masolatbol"}}</button>
|
||
</form>
|
||
{{end}}
|
||
<a href="{{appPageURL .Slug}}/data" class="btn btn-xs btn-outline">{{T "backups_apps.beallitas"}}</a>
|
||
</div>
|
||
{{else}}
|
||
<span class="layer-auto-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg> {{T "backups_apps.1_mentes_auto"}}</span>
|
||
<span class="layer-unconfigured"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "backups_apps.nincs_2_off_drive_masolat"}}</span>
|
||
{{if .Tier2LastError}}
|
||
<span class="layer-reason" style="opacity:.85" title="{{T "backups_apps.a_2_mentes_automatikus_kulon"}}">{{.Tier2LastError}}</span>
|
||
{{end}}
|
||
<div class="layer-actions">
|
||
<a href="{{appPageURL .Slug}}/data" class="btn btn-xs btn-outline">{{T "backups_apps.beallitas"}}</a>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
<!-- Tier 3: Off-box (NAS) backup — real state (see tier3State). -->
|
||
{{if eq .Tier3State "active"}}
|
||
<div class="backup-layer-row">
|
||
<span class="tier-label">{{T "backups_apps.3_mentes"}}</span>
|
||
<span class="layer-badge {{if eq $.Offbox.LastStatus "ok"}}text-ok{{else if eq $.Offbox.LastStatus "error"}}text-error{{else if eq $.Offbox.LastStatus "running"}}text-muted{{end}}">{{if eq $.Offbox.LastStatus "ok"}}{{T "backups_apps.sikeres"}}{{else if eq $.Offbox.LastStatus "error"}}{{T "backups_apps.hiba"}}{{else if eq $.Offbox.LastStatus "running"}}{{T "backups_apps.fut"}}{{else}}—{{end}}</span>
|
||
<span class="tier-location">restic → {{$.Offbox.Host}}</span>
|
||
{{if $.Offbox.LastRun}}<span class="layer-last">{{T "backups_apps.utolso_4"}}</span>{{end}}
|
||
<span class="tier-contents">{{T "backups_apps.helyreallitasi_egyseg_titkositva"}}</span>
|
||
</div>
|
||
{{else if eq .Tier3State "escrow_pending"}}
|
||
<div class="backup-layer-row">
|
||
<span class="tier-label">{{T "backups_apps.3_mentes"}}</span>
|
||
<span class="layer-badge text-error">{{T "backups_apps.kulcsletetre_var"}}</span>
|
||
<span class="tier-contents">{{T "backups_apps.a_tavoli_mentes_a_titkositasi"}}</span>
|
||
</div>
|
||
{{else if eq .Tier3State "off"}}
|
||
<div class="backup-layer-row" style="opacity:.5">
|
||
<span class="tier-label">{{T "backups_apps.3_mentes"}}</span>
|
||
<span class="layer-badge">{{T "backups_apps.kikapcsolva"}}</span>
|
||
<span class="tier-contents">{{T "backups_apps.ez_az_alkalmazas_nincs_kijelolve"}}</span>
|
||
<div class="layer-actions">
|
||
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">{{T "backups_apps.bekapcsolas"}}</a>
|
||
</div>
|
||
</div>
|
||
{{else}}
|
||
<div class="backup-layer-row" style="opacity:.5">
|
||
<span class="tier-label">{{T "backups_apps.3_mentes"}}</span>
|
||
<span class="layer-badge" style="background:var(--bg-2);color:var(--text-3)">{{T "backups_apps.nincs_beallitva"}}</span>
|
||
<span class="tier-location">{{T "backups_apps.tavoli_offsite"}}</span>
|
||
<span class="tier-contents">{{T "backups_apps.nincs_tavoli_mentesi_cel_beallitva"}}</span>
|
||
<div class="layer-actions">
|
||
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">{{T "backups_apps.beallitas"}}</a>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{if .Warnings}}
|
||
<div class="layer-warnings">
|
||
{{range .Warnings}}
|
||
<div class="backup-layer-warning">{{.}}</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
</div>
|
||
{{end}}
|
||
|
||
{{end}}
|
||
|
||
<script>
|
||
{{template "restore_banner_js"}}
|
||
function toggleBackupDetail(header) {
|
||
var detail = header.nextElementSibling;
|
||
var icon = header.querySelector('.expand-icon');
|
||
if (detail.style.display === 'none') {
|
||
detail.style.display = 'block';
|
||
icon.textContent = '▼';
|
||
} else {
|
||
detail.style.display = 'none';
|
||
icon.textContent = '▶';
|
||
}
|
||
}
|
||
|
||
function triggerBackupFromPage() {
|
||
const btn = document.getElementById('backup-page-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = '{{T "backups_apps.mentes_inditasa"}}';
|
||
fetch('/api/backup/run', { method: 'POST', headers: csrfHeaders() })
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.ok) {
|
||
btn.textContent = '{{T "backups_apps.mentes_folyamatban"}}';
|
||
btn.classList.add('loading');
|
||
startBackupPolling();
|
||
} else {
|
||
btn.textContent = data.error || '{{T "backups_apps.hiba"}}';
|
||
setTimeout(() => { btn.textContent = '{{T "backups_apps.mentes_most"}}'; btn.disabled = false; }, 3000);
|
||
}
|
||
})
|
||
.catch(() => {
|
||
btn.textContent = '{{T "backups_apps.hiba"}}';
|
||
setTimeout(() => { btn.textContent = '{{T "backups_apps.mentes_most"}}'; btn.disabled = false; }, 3000);
|
||
});
|
||
}
|
||
|
||
function startBackupPolling() {
|
||
const poll = setInterval(() => {
|
||
fetch('/api/backup/status')
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.ok && data.data && !data.data.running) {
|
||
clearInterval(poll);
|
||
window.location.reload();
|
||
}
|
||
})
|
||
.catch(() => {});
|
||
}, 3000);
|
||
}
|
||
|
||
// Auto-poll if backup is already running on page load
|
||
{{if .Backup}}{{if .Backup.Running}}
|
||
startBackupPolling();
|
||
{{end}}{{end}}
|
||
</script>
|
||
|
||
{{template "layout_end" .}}
|
||
{{end}}
|