Files
bvandeusenandClaude Opus 5.5 fb99f8b551
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m27s
CI / unit (push) Failing after 1m37s
CI / publish (push) Skipped
feat(backup): restore from Settings → Backups, with restoring mode (#5816, #5817)
#5816 "Restoring mode": while a backup goes back in, the scheduler holds,
ingest and HTMX answer 503 with Retry-After (agents keep their samples),
and every page says what is happening. A safety backup of Steward as it
stands is made first, to /data/backups and the bucket. The backup's key
is put in place, the restore audited, and Steward exits with code 3 so
Swarm, compose or systemd starts it fresh. A failed in-place restore
changes nothing and says why on Backups; a full replace that fails after
the wipe restarts into first-run setup.

#5817 "Settings → Backups: Restore": a Restore button on each sent backup
and on the file card. The chosen backup is checked (the same comparison
as Check, now a shared partial) and held server-side for an hour under a
token; it starts only once the admin types "restore", and an older
backup also needs "Replace everything" ticked. A newer backup, or one
whose key can't open its secrets, gets no button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:03:01 -04:00

62 lines
3.7 KiB
HTML

{# settings/_backup_report.html — what a check found (#5823), shown on the check
page and on the restore confirm page (#5817): the verdict, what the backup
is, each area's count, then each difference. Needs `report`. #}
{% set tone = "tone-crit" if not report.ok else ("tone-ok" if not report.changes else "tone-warn") %}
<p class="page-verdict"><span class="{{ tone }}">{{ report.summary() }}</span></p>
<div class="card" style="max-width:640px;margin-bottom:1.25rem;">
<dl class="kv">
<dt>File</dt><dd>{{ report.name }}</dd>
<dt>Made</dt><dd>{% if report.made_at %}{{ report.made_at|when }}{% else %}—{% endif %}
by Steward {{ report.manifest.steward_version or "—" }}</dd>
<dt>Schema</dt><dd>
{%- if report.schema == "same" %}The same as this Steward's.
{%- elif report.schema == "older" %}<span class="tone-warn">Older than this Steward's.</span> A restore would replace everything, and Steward would bring it up to date on start.
{%- else %}<span class="tone-crit">Newer than this Steward's.</span> This Steward can't restore it; update Steward first.{% endif %}</dd>
<dt>Secret key</dt><dd>
{%- if report.key_same %}The one in use here.{% else %}<span class="tone-warn">A different one.</span> A restore would put it in place, and passwords and keys stored since would need entering again.{% endif %}
{%- if report.sealed %} It opens {{ report.sealed - report.unopened }} of the backup's {{ report.sealed }} sealed value{{ "s" if report.sealed != 1 }}.{% endif %}</dd>
</dl>
</div>
<div class="card-flush" style="margin-bottom:1.25rem;">
<table class="table">
<thead><tr><th>Area</th><th>Same</th><th>Differ</th><th>Only in the backup</th><th>Only here</th></tr></thead>
<tbody>
{% for a in report.areas %}
{% set differ = a.tables|sum(attribute="differ", start=[])|length %}
{% set only_b = a.tables|sum(attribute="only_backup", start=[])|length %}
{% set only_h = a.tables|sum(attribute="only_here", start=[])|length %}
<tr>
<td>{{ a.name }}</td>
<td>{{ a.same }}</td>
<td>{% if differ %}<span class="tone-warn">{{ differ }}</span>{% else %}—{% endif %}</td>
<td>{% if only_b %}<span class="tone-warn">{{ only_b }}</span>{% else %}—{% endif %}</td>
<td>{% if only_h %}<span class="tone-warn">{{ only_h }}</span>{% else %}—{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% for a in report.areas if a.changes or a.tables|selectattr("note")|list %}
<h2 class="section-title" style="margin:1.5rem 0 0.75rem;">{{ a.name }}</h2>
<ul class="check-list">
{% for t in a.tables %}
{% for d in t.differ %}
<li><strong>{{ d.label }}</strong> differs
{%- if d.secret %} (a sealed value; not shown).
{%- elif d.backup_value is not none or d.here_value is not none %}: the backup has <code>{{ d.backup_value if d.backup_value is not none else "nothing" }}</code>, here it is <code>{{ d.here_value if d.here_value is not none else "nothing" }}</code>.
{%- else %} in {{ d.columns|join(", ") }}.{% endif %}</li>
{% endfor %}
{% for label in t.only_backup %}<li><strong>{{ label }}</strong> is only in the backup: a restore brings it back.</li>{% endfor %}
{% for label in t.only_here %}<li><strong>{{ label }}</strong> is only here: a restore removes it.</li>{% endfor %}
{% if t.note %}<li class="tone-dim">{{ t.table }}: {{ t.note }}</li>{% endif %}
{% endfor %}
</ul>
{% endfor %}
<p class="field-hint" style="margin-top:1rem;">Left out of the comparison: what Steward records as it runs
(when an agent or monitor was last seen, a schedule's last run, the backup history), since it changes by the
minute. A restore still brings those back as they were in the backup.</p>