Files
felhom.eu/documentation/audits/update-night-2026-09-21/bad-days/P4-morning-after/backups-apps-en.html
T
admin 9c69b3ff07
gates / gates (push) Successful in 27s
Update night: Phases 2-4 evidence — both engines, the unattended HOLD, and five new findings
Evidence off the machine at the end of the phases that produced it (R-320). Teardown follows.

PHASE 2 — the two database engines, through the REAL Update button:
- MariaDB 11.6 -> 12.3 on nextcloud: PROVEN, and pressed through the button for the first time.
  All four SPIKE-r459 observables: the datadir's own record moved 11.6.2 -> 12.3.3; the engine
  itself says "already upgraded ... no need to run mariadb-upgrade again"; the entrypoint says
  "Major version upgrade detected ... Check required!" and then STARTED and FINISHED it (not the
  `skipped due to $MARIADB_AUTO_UPGRADE` line R-459 feared); and the engine took its own
  pre-upgrade backup, 631 905 B. The seeded Nextcloud account read back.
- PostgreSQL 16 -> 17 on docmost: FAILED exactly as R-463 predicted and nobody had measured.
  5.1 s to held; the pin named 17 while nothing ran; the restore brought it back in 29.1 s.
  The engine's REFUSAL LINE was destroyed by failAndHold before any probe could read it, so it
  was REPRODUCED INDEPENDENTLY with a control on every step (R-320).

PHASE 3 — the bad days. B1 produced THE UNATTENDED HOLD, which this project has never had: the
caller pressed once with nobody watching, the app held after 312.9 s, and passes 2 and 3 pressed
nothing. B2 put the pin back on a pull failure in 1.0 s. B3 refused `busy` six times. B4 showed
there is NO single-flight — 5 of 5 updates ran at once and all ended honest. B5 cut the power in
`backing-up` and the box recovered itself and said so. B7 refused under the 2 GB floor. B9 found
R-458's risk narrower than the row states.

PHASE 4 — every badge on the box is TRUE, and the held app answers all four of Q4's questions.

FINDINGS, five new and three corrections to existing rows. The one that matters: R-618 is P1 —
two templates name a health probe the app does not answer, and because the guarded update waits
on that same probe, a SUCCESSFUL update ends by STOPPING a working app. Measured: tandoor served
HTTP 200 on the new version at four samples across five minutes and was then stopped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 22:13:57 +02:00

1431 lines
78 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Backup — Apps — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.261.0">
<link rel="stylesheet" href="/static/style.css?v=0.261.0">
<meta name="csrf-token" content="723f37f7def1b73c3149054da3878d7f992f9036c45461296a983a34699d4660">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Yes';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Cancel';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Message</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.261.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menu">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.261.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Launcher</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Dashboard</a></li>
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Apps</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Storage<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Drives</a></li>
<li><a href="/storage/network" class="">Network storage</a></li>
</ul>
</li>
<li class="nav-group is-open">
<button type="button" class="nav-group-toggle active" aria-expanded="true" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Backup<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Overview</a></li>
<li><a href="/backups/remote" class="">Remote backup</a></li>
<li><a href="/backups/apps" class="active">Apps</a></li>
<li><a href="/backups/restore" class="">Restore</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Sharing<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Network sharing</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>System monitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Settings</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>System</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Notifications</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Security and access</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.261.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Language" title="Language"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="723f37f7def1b73c3149054da3878d7f992f9036c45461296a983a34699d4660"><input type="hidden" name="back" value="/backups/apps"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="723f37f7def1b73c3149054da3878d7f992f9036c45461296a983a34699d4660"><input type="hidden" name="back" value="/backups/apps"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item current" aria-current="true">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Sign out ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="alerts-container">
<div class="alert-banner alert-banner-warning">
<span class="alert-icon"><svg class="ico"><use href="#i-triangle-alert"/></svg></span>
<span class="alert-message">The hub connection is off — central monitoring is not running</span>
<a href="/monitoring" class="alert-link">System monitor →</a>
</div>
</div>
<div class="page-header">
<h2>Backup — Apps</h2>
<span class="domain-badge">enkisfelhom.hu</span>
</div>
<div id="restore-banner" class="flash" style="display:none"></div>
<h3 class="backup-tier-divider">App backups (database + configuration)</h3>
<p class="form-hint" style="margin:-0.25rem 0 1rem">A detailed backup of each app — database dumps, settings and app files. Independent of the full backup above, it can be restored per app.</p>
<div class="schedule-card">
<h3>Schedule</h3>
<div class="schedule-rows">
<div class="schedule-row">
<span class="schedule-task">Database backup</span>
<span class="schedule-time">02:30</span>
<span class="schedule-next">Next: tomorrow 02:30</span>
</div>
</div>
<div class="schedule-summary">
<div class="schedule-summary-row">
<span>Last database backup:</span>
<span class="schedule-summary-value">2026-09-21 22:05 (3 minutes ago)</span>
</div>
</div>
<div class="schedule-actions">
<button class="btn btn-sm btn-primary" onclick="triggerBackupFromPage()" id="backup-page-btn"
>
Back up now
</button>
</div>
</div>
<div class="backup-section-card">
<h3>Databases</h3>
<div class="backup-table-wrap">
<table class="db-table">
<thead>
<tr>
<th>App</th>
<th>Type</th>
<th>Size</th>
<th>Last</th>
<th>Validation</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>nextcloud</td>
<td><span class="db-type-badge db-type-mariadb">MariaDB</span></td>
<td class="mono">516.6 KB</td>
<td class="mono">22:05</td>
<td>
<span class="validation-badge validation-ok">131 tables</span>
</td>
<td>
<span class="validation-badge validation-ok">OK</span>
</td>
</tr>
<tr>
<td>nextcloud</td>
<td><span class="db-type-badge db-type-mariadb">MariaDB</span></td>
<td class="mono">504.7 KB</td>
<td class="mono">22:05</td>
<td>
<span class="validation-badge validation-ok">131 tables</span>
</td>
<td>
<span class="validation-badge validation-ok">OK</span>
</td>
</tr>
<tr>
<td>zipline</td>
<td><span class="db-type-badge db-type-postgres">PostgreSQL</span></td>
<td class="mono">39.8 KB</td>
<td class="mono">22:05</td>
<td>
<span class="validation-badge validation-ok">17 tables</span>
</td>
<td>
<span class="validation-badge validation-ok">OK</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="backup-section-card">
<h3>Backup status of the apps</h3>
<div class="app-backup-row" data-status="green">
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<img class="app-row-icon" src="/static/assets/bentopdf-logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/bentopdf-logo.png';}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">BentoPDF</span>
<div class="app-backup-row-meta">
<span class="meta-badge">Config</span>
<span class="status-dot status-green" title="App data backup is fine"></span>
</div>
<span class="expand-icon">▶</span>
</div>
<div class="app-backup-row-detail" style="display:none">
<div class="backup-layers">
<div class="backup-layer-row">
<span class="tier-label">Backup 1</span>
<span class="layer-badge">Auto</span>
<span class="tier-location">local</span>
<span class="layer-last">Last: just now
<span class="state-text-neutral" title="We have no result for this backup.">—</span>
</span>
<span class="tier-contents">Config</span>
</div>
<div class="backup-layer-row">
<span class="tier-label">Backup 2</span>
<span class="layer-method">rsync</span>
<span class="layer-dest">→ scratch_hdd</span>
<span class="layer-schedule">Daily</span>
<span class="layer-last">Last successful: just now
<span class="text-ok">
Done
</span>
</span>
<span class="tier-size">13.2 KB</span>
<span class="tier-contents">Config</span>
<span class="tier-browsable" title="The backup is a browsable file system"><svg class="ico ico-sm"><use href="#i-file-text"/></svg></span>
<div class="layer-actions">
<form method="POST" action="/backup/tier2/restore" style="display:inline"><input type="hidden" name="_csrf" value="723f37f7def1b73c3149054da3878d7f992f9036c45461296a983a34699d4660">
<input type="hidden" name="stack_name" value="bentopdf">
<button type="submit" class="btn btn-xs btn-outline" data-confirm="Restore the missing files from the secondary copy? Existing files are NOT changed and NOT deleted. The app stops during the operation. Last successful copy: 2026-09-21 22:08.">Restore files</button>
</form>
<form method="POST" action="/backup/tier2/unit-restore" style="display:inline"><input type="hidden" name="_csrf" value="723f37f7def1b73c3149054da3878d7f992f9036c45461296a983a34699d4660">
<input type="hidden" name="stack_name" value="bentopdf">
<button type="submit" class="btn btn-xs btn-danger-outline" data-confirm="Ez a művelet FELÜLÍRJA az alkalmazás jelenlegi adatait – az adatbázisát és a belső köteteit is – a második meghajtón lévő másolattal. Ami a másolat óta keletkezett, elveszik. A másolat kelte: 2026-09-21 22:08. A mellette lévő „Fájlok visszaállítása” ezzel szemben csak a hiányzó fájlokat pótolja, és semmit nem ír felül. Az alkalmazás a művelet idejére leáll.">Full restore from the copy</button>
</form>
<a href="/stacks/bentopdf/backup" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">Backup 3</span>
<span class="layer-badge" style="background:var(--bg-2);color:var(--text-3)">Not set up</span>
<span class="tier-location">remote (offsite)</span>
<span class="tier-contents">No remote backup target set up</span>
<div class="layer-actions">
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
</div>
</div>
</div>
<div class="app-backup-row" data-status="green">
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<img class="app-row-icon" src="/static/assets/gitea-logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/gitea-logo.png';}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">Gitea</span>
<div class="app-backup-row-meta">
<span class="meta-badge">Config + Data</span>
<span class="status-dot status-green" title="App data backup is fine"></span>
</div>
<span class="expand-icon">▶</span>
</div>
<div class="app-backup-row-detail" style="display:none">
<div class="backup-layers">
<div class="backup-layer-row">
<span class="tier-label">Backup 1</span>
<span class="layer-badge">Auto</span>
<span class="tier-location">local</span>
<span class="layer-last">Last: 3 minutes ago
<span class="state-text-neutral" title="We have no result for this backup.">—</span>
</span>
<span class="tier-contents">Config &#43; Data</span>
</div>
<div class="backup-layer-row">
<span class="tier-label">Backup 2</span>
<span class="layer-auto-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg> Backup 1 auto</span>
<span class="layer-unconfigured"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> No 2nd (off-drive) copy</span>
<div class="layer-actions">
<a href="/stacks/gitea/backup" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">Backup 3</span>
<span class="layer-badge" style="background:var(--bg-2);color:var(--text-3)">Not set up</span>
<span class="tier-location">remote (offsite)</span>
<span class="tier-contents">No remote backup target set up</span>
<div class="layer-actions">
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
</div>
</div>
</div>
<div class="app-backup-row" data-status="red">
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<img class="app-row-icon" src="/static/assets/glance-logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/glance-logo.png';}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">Glance</span>
<div class="app-backup-row-meta">
<span class="meta-badge">Config + Data</span>
<span class="status-dot status-red" title="A(z) glance frissítése 2026-09-21 21:53-kor nem sikerült, és az alkalmazás nem indult el az új verzióval. Az alkalmazás biztonsági okból leállítva marad, hogy az adatai ne sérüljenek. Visszaállítható a Mentések oldalon ebből a biztonsági mentésből: saját meghajtó, 2026-09-21 21:47 — ez a másolat a beállításokat, az adatbázist és az adatköteteket tartalmazza."></span>
</div>
<span class="expand-icon">▶</span>
</div>
<div class="app-backup-row-detail" style="display:none">
<div class="backup-layers">
<div class="backup-layer-row">
<span class="tier-label">Backup 1</span>
<span class="layer-badge">Auto</span>
<span class="tier-location">local</span>
<span class="layer-last">Last: 21 minutes ago
<span class="state-text-neutral" title="We have no result for this backup.">—</span>
</span>
<span class="tier-contents">Config &#43; Data</span>
</div>
<div class="backup-layer-row">
<span class="tier-label">Backup 2</span>
<span class="layer-auto-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg> Backup 1 auto</span>
<span class="layer-unconfigured"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> No 2nd (off-drive) copy</span>
<div class="layer-actions">
<a href="/stacks/glance/backup" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">Backup 3</span>
<span class="layer-badge" style="background:var(--bg-2);color:var(--text-3)">Not set up</span>
<span class="tier-location">remote (offsite)</span>
<span class="tier-contents">No remote backup target set up</span>
<div class="layer-actions">
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
</div>
</div>
</div>
<div class="app-backup-row" data-status="green">
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<img class="app-row-icon" src="/static/assets/nextcloud-logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/nextcloud-logo.png';}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">Nextcloud</span>
<div class="app-backup-row-meta">
<span class="meta-badge meta-badge-storage">SCRATCH HDD</span>
<span class="mono app-backup-size" style="font-size:.8rem">62.3 MB</span>
<span class="status-dot status-green" title="App data backup is fine"></span>
</div>
<span class="expand-icon">▶</span>
</div>
<div class="app-backup-row-detail" style="display:none">
<div class="backup-layers">
<div class="backup-layer-row">
<span class="tier-label">Backup 1</span>
<span class="layer-badge">Auto</span>
<span class="tier-location">local</span>
<span class="layer-last">Last: 3 minutes ago
<span class="text-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg></span>
</span>
<span class="tier-contents">DB &#43; Config</span>
<span class="state-text-neutral" style="font-size:.8rem">Az alkalmazás fájljairól jelenleg nincs másolat — kapcsold be a távoli mentést vagy adj hozzá második meghajtót. <a href="/backups/remote">Távoli mentés beállítása &rarr;</a></span>
</div>
<div class="backup-layer-row">
<span class="tier-label">Backup 2</span>
<span class="layer-auto-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg> Backup 1 auto</span>
<span class="layer-unconfigured"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> No 2nd (off-drive) copy</span>
<div class="layer-actions">
<a href="/stacks/nextcloud/backup" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">Backup 3</span>
<span class="layer-badge" style="background:var(--bg-2);color:var(--text-3)">Not set up</span>
<span class="tier-location">remote (offsite)</span>
<span class="tier-contents">No remote backup target set up</span>
<div class="layer-actions">
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
</div>
</div>
</div>
<div class="app-backup-row" data-status="green">
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<img class="app-row-icon" src="/static/assets/opengist-logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}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">OpenGist</span>
<div class="app-backup-row-meta">
<span class="meta-badge">Config + Data</span>
<span class="status-dot status-green" title="App data backup is fine"></span>
</div>
<span class="expand-icon">▶</span>
</div>
<div class="app-backup-row-detail" style="display:none">
<div class="backup-layers">
<div class="backup-layer-row">
<span class="tier-label">Backup 1</span>
<span class="layer-badge">Auto</span>
<span class="tier-location">local</span>
<span class="layer-last">Last: 1 minute ago
<span class="state-text-neutral" title="We have no result for this backup.">—</span>
</span>
<span class="tier-contents">Config &#43; Data</span>
</div>
<div class="backup-layer-row">
<span class="tier-label">Backup 2</span>
<span class="layer-auto-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg> Backup 1 auto</span>
<span class="layer-unconfigured"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> No 2nd (off-drive) copy</span>
<div class="layer-actions">
<a href="/stacks/opengist/backup" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">Backup 3</span>
<span class="layer-badge" style="background:var(--bg-2);color:var(--text-3)">Not set up</span>
<span class="tier-location">remote (offsite)</span>
<span class="tier-contents">No remote backup target set up</span>
<div class="layer-actions">
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
</div>
</div>
</div>
<div class="app-backup-row" data-status="green">
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<img class="app-row-icon" src="/static/assets/privatebin-logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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">PrivateBin</span>
<div class="app-backup-row-meta">
<span class="meta-badge">Config + Data</span>
<span class="status-dot status-green" title="App data backup is fine"></span>
</div>
<span class="expand-icon">▶</span>
</div>
<div class="app-backup-row-detail" style="display:none">
<div class="backup-layers">
<div class="backup-layer-row">
<span class="tier-label">Backup 1</span>
<span class="layer-badge">Auto</span>
<span class="tier-location">local</span>
<span class="layer-last">Last: 1 minute ago
<span class="state-text-neutral" title="We have no result for this backup.">—</span>
</span>
<span class="tier-contents">Config &#43; Data</span>
</div>
<div class="backup-layer-row">
<span class="tier-label">Backup 2</span>
<span class="layer-auto-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg> Backup 1 auto</span>
<span class="layer-unconfigured"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> No 2nd (off-drive) copy</span>
<div class="layer-actions">
<a href="/stacks/privatebin/backup" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">Backup 3</span>
<span class="layer-badge" style="background:var(--bg-2);color:var(--text-3)">Not set up</span>
<span class="tier-location">remote (offsite)</span>
<span class="tier-contents">No remote backup target set up</span>
<div class="layer-actions">
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
</div>
</div>
</div>
<div class="app-backup-row" data-status="green">
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<img class="app-row-icon" src="/static/assets/uptime-kuma-logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/uptime-kuma-logo.png';}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">Uptime Kuma</span>
<div class="app-backup-row-meta">
<span class="meta-badge">Config + Data</span>
<span class="status-dot status-green" title="App data backup is fine"></span>
</div>
<span class="expand-icon">▶</span>
</div>
<div class="app-backup-row-detail" style="display:none">
<div class="backup-layers">
<div class="backup-layer-row">
<span class="tier-label">Backup 1</span>
<span class="layer-badge">Auto</span>
<span class="tier-location">local</span>
<span class="layer-last">Last: 1 minute ago
<span class="state-text-neutral" title="We have no result for this backup.">—</span>
</span>
<span class="tier-contents">Config &#43; Data</span>
</div>
<div class="backup-layer-row">
<span class="tier-label">Backup 2</span>
<span class="layer-auto-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg> Backup 1 auto</span>
<span class="layer-unconfigured"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> No 2nd (off-drive) copy</span>
<div class="layer-actions">
<a href="/stacks/uptime-kuma/backup" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">Backup 3</span>
<span class="layer-badge" style="background:var(--bg-2);color:var(--text-3)">Not set up</span>
<span class="tier-location">remote (offsite)</span>
<span class="tier-contents">No remote backup target set up</span>
<div class="layer-actions">
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
</div>
</div>
</div>
<div class="app-backup-row" data-status="green">
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<img class="app-row-icon" src="/static/assets/vaultwarden-logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/vaultwarden-logo.png';}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">Vaultwarden</span>
<div class="app-backup-row-meta">
<span class="meta-badge">Config + Data</span>
<span class="status-dot status-green" title="App data backup is fine"></span>
</div>
<span class="expand-icon">▶</span>
</div>
<div class="app-backup-row-detail" style="display:none">
<div class="backup-layers">
<div class="backup-layer-row">
<span class="tier-label">Backup 1</span>
<span class="layer-badge">Auto</span>
<span class="tier-location">local</span>
<span class="layer-last">Last: 3 minutes ago
<span class="state-text-neutral" title="We have no result for this backup.">—</span>
</span>
<span class="tier-contents">Config &#43; Data</span>
</div>
<div class="backup-layer-row">
<span class="tier-label">Backup 2</span>
<span class="layer-auto-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg> Backup 1 auto</span>
<span class="layer-unconfigured"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> No 2nd (off-drive) copy</span>
<div class="layer-actions">
<a href="/stacks/vaultwarden/backup" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">Backup 3</span>
<span class="layer-badge" style="background:var(--bg-2);color:var(--text-3)">Not set up</span>
<span class="tier-location">remote (offsite)</span>
<span class="tier-contents">No remote backup target set up</span>
<div class="layer-actions">
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
</div>
</div>
</div>
<div class="app-backup-row" data-status="green">
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<img class="app-row-icon" src="/static/assets/wishlist-logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/wishlist-logo.png';}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">Wishlist</span>
<div class="app-backup-row-meta">
<span class="meta-badge">Config + Data</span>
<span class="status-dot status-green" title="App data backup is fine"></span>
</div>
<span class="expand-icon">▶</span>
</div>
<div class="app-backup-row-detail" style="display:none">
<div class="backup-layers">
<div class="backup-layer-row">
<span class="tier-label">Backup 1</span>
<span class="layer-badge">Auto</span>
<span class="tier-location">local</span>
<span class="layer-last">Last: 1 minute ago
<span class="state-text-neutral" title="We have no result for this backup.">—</span>
</span>
<span class="tier-contents">Config &#43; Data</span>
</div>
<div class="backup-layer-row">
<span class="tier-label">Backup 2</span>
<span class="layer-auto-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg> Backup 1 auto</span>
<span class="layer-unconfigured"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> No 2nd (off-drive) copy</span>
<div class="layer-actions">
<a href="/stacks/wishlist/backup" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">Backup 3</span>
<span class="layer-badge" style="background:var(--bg-2);color:var(--text-3)">Not set up</span>
<span class="tier-location">remote (offsite)</span>
<span class="tier-contents">No remote backup target set up</span>
<div class="layer-actions">
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
</div>
</div>
</div>
<div class="app-backup-row" data-status="green">
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<img class="app-row-icon" src="/static/assets/zipline-logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/zipline-logo.png';}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">Zipline</span>
<div class="app-backup-row-meta">
<span class="meta-badge">Config + DB + Data</span>
<span class="status-dot status-green" title="App data backup is fine"></span>
</div>
<span class="expand-icon">▶</span>
</div>
<div class="app-backup-row-detail" style="display:none">
<div class="backup-layers">
<div class="backup-layer-row">
<span class="tier-label">Backup 1</span>
<span class="layer-badge">Auto</span>
<span class="tier-location">local</span>
<span class="layer-last">Last: 2 minutes ago
<span class="text-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg></span>
</span>
<span class="tier-contents">DB &#43; Config &#43; Data</span>
</div>
<div class="backup-layer-row">
<span class="tier-label">Backup 2</span>
<span class="layer-auto-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg> Backup 1 auto</span>
<span class="layer-unconfigured"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> No 2nd (off-drive) copy</span>
<div class="layer-actions">
<a href="/stacks/zipline/backup" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">Backup 3</span>
<span class="layer-badge" style="background:var(--bg-2);color:var(--text-3)">Not set up</span>
<span class="tier-location">remote (offsite)</span>
<span class="tier-contents">No remote backup target set up</span>
<div class="layer-actions">
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">Settings</a>
</div>
</div>
</div>
</div>
</div>
<div class="app-backup-row" data-status="yellow">
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<img class="app-row-icon" src="/static/assets/paperless-ngx-logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/paperless-ngx-logo.png';}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">Paperless-ngx</span>
<div class="app-backup-row-meta">
<span class="tag tag-warn">Removed — restorable</span>
<span class="meta-badge meta-badge-storage">SCRATCH HDD</span>
<span class="status-dot status-yellow" title="Removed app — its backup is here and can be restored"></span>
</div>
<span class="expand-icon">▶</span>
</div>
<div class="app-backup-row-detail" style="display:none">
<div class="backup-layers">
<div class="backup-layer-row">
<span class="tier-label">Backup</span>
<span class="layer-badge">Kept</span>
<span class="tier-location">local</span>
<span class="tier-contents">You removed the app; its backup was kept. Last backup: 2026-09-15 11:21.</span>
<div class="layer-actions">
<form method="POST" action="/backup/restore" style="display:inline"><input type="hidden" name="_csrf" value="723f37f7def1b73c3149054da3878d7f992f9036c45461296a983a34699d4660">
<input type="hidden" name="stack_name" value="paperless-ngx">
<input type="hidden" name="snapshot_id" value="helyi">
<button type="submit" class="btn btn-xs btn-outline" data-confirm="Reinstall Paperless-ngx from the kept backup (2026-09-15 11:21)? The data comes back from the backup.">Restore from the backup</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
(function(){
var banner = document.getElementById('restore-banner');
if (!banner) return;
var sawRunning = false;
function opLabel(op){ return op === 'tier2-restore' ? 'File restore'
: op === 'offbox-restore' ? 'Remote restore' : 'Restore'; }
function render(st){
if (st.running) {
sawRunning = true;
banner.className = 'flash';
banner.style.display = 'block';
banner.textContent = opLabel(st.op) + ' in progress' + (st.stack ? ': ' + st.stack : '') + '…';
return;
}
if (st.last && (sawRunning || st.last_recent)) {
banner.style.display = 'block';
if (st.last.ok) {
banner.className = 'flash flash-success';
banner.textContent = st.last.message || (opLabel(st.last.op) + ' done.');
} else {
banner.className = 'flash flash-error';
banner.textContent = st.last.message || (opLabel(st.last.op) + ' failed.');
}
}
}
function poll(){
fetch('/api/backup/restore-status', {headers: {'Accept':'application/json'}})
.then(function(r){ return r.json(); })
.then(function(j){ if (j && j.data) render(j.data); })
.catch(function(){});
}
poll();
setInterval(poll, 3000);
})();
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 = 'Starting backup...';
fetch('/api/backup/run', { method: 'POST', headers: csrfHeaders() })
.then(r => r.json())
.then(data => {
if (data.ok) {
btn.textContent = 'Backup in progress...';
btn.classList.add('loading');
startBackupPolling();
} else {
btn.textContent = data.error || 'Error';
setTimeout(() => { btn.textContent = 'Back up now'; btn.disabled = false; }, 3000);
}
})
.catch(() => {
btn.textContent = 'Error';
setTimeout(() => { btn.textContent = 'Back up now'; 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);
}
</script>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('This app is already installed.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Refreshing...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Templates refreshed') : ('Error: ' + (data.error || 'Unknown error'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Network error: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'In progress...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Error: ' + (data.error || 'Unknown error'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Network error: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Your data on the hard drive:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Delete your data from the hard drive</label>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Delete app: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">This removes the containers, the volumes and the configuration files.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Cancel</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Delete</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Close</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Deleting...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Deleted!</h3>' +
'<p style="color:var(--text-2)">The app (' + name + ') was deleted.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
} else {
showAlert('Error: ' + (data.error || 'Unknown error'));
btn.disabled = false;
btn.textContent = 'Delete';
}
} catch (err) {
showAlert('Network error: ' + err.message);
btn.disabled = false;
btn.textContent = 'Delete';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
sections += '<div class="modal-section">' +
'<strong>Always deleted:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker volumes (database, app configuration)</li>' +
'<li>Install configuration (app.yaml)</li>' +
'<li>Secondary backup schedule</li>' +
'</ul></div>';
var hddCheckbox = '';
if (hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Your data on the hard drive:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Delete your data</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'If you reinstall the app, you need to import the data again, because the database is deleted. Data you keep is NOT backed up automatically any more.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Backup data:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Delete backup data</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'The nightly restic snapshots cannot be deleted one by one — they expire automatically under the retention rules.' +
'</div></div>';
}
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Remove app: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">The app goes back to "Not installed". The template stays, and you can install it again.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Cancel</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Remove</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Close</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Removing...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted backups: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Backups not deleted: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Removed!</h3>' +
'<p style="color:var(--text-2)">The app (' + name + ') was removed. You can install it again with the Install button.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
} else {
showAlert('Error: ' + (data.error || 'Unknown error'));
btn.disabled = false;
btn.textContent = 'Remove';
}
} catch (err) {
showAlert('Network error: ' + err.message);
btn.disabled = false;
btn.textContent = 'Remove';
}
}
</script>
</body>
</html>