add database backup and restore admin page
Some checks failed
deploy / deploy (push) Has been cancelled

- SQLCipher-encrypted backup creation via VACUUM INTO
- Backup history with auto-pruning (keeps last 5)
- Pre-restore automatic backup for safety
- Restore from history or uploaded file
- Stats display with table breakdown
- Download hook for client-side file download
- SECRET_KEY_DB config for encryption at rest

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-03-13 13:33:29 +00:00
parent b0f8eea2bc
commit 09f55dfe67
10 changed files with 2183 additions and 1 deletions

View File

@@ -5158,6 +5158,17 @@
color: var(--t-status-error, oklch(0.6 0.2 25));
}
.admin-btn-danger {
background: oklch(0.55 0.2 25);
border-color: oklch(0.55 0.2 25);
color: white;
&:hover:not(:disabled) {
background: oklch(0.5 0.22 25);
border-color: oklch(0.5 0.22 25);
}
}
/* ── Provider group headings ── */
.card-radio-group-heading {
@@ -5840,4 +5851,269 @@
.sm\:scale-100 { scale: 1; }
}
/* ── Backup page ── */
.admin-backup {
max-width: 48rem;
}
.admin-link {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
color: var(--t-primary);
cursor: pointer;
background: none;
border: none;
padding: 0;
&:hover {
text-decoration: underline;
}
}
.admin-error {
color: var(--admin-error);
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.admin-table-compact {
font-size: 0.8125rem;
th, td {
padding: 0.5rem 0.75rem;
}
}
.backup-tables {
margin-top: 1rem;
max-height: 20rem;
overflow-y: auto;
border: 1px solid var(--t-border-subtle);
border-radius: var(--radius-md);
}
.backup-tables .admin-table {
margin: 0;
}
.backup-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
.backup-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.backup-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 0.625rem 0.875rem;
background: var(--t-surface-raised);
border-radius: var(--radius-md);
}
.backup-item-info {
display: flex;
flex-direction: column;
gap: 0.125rem;
min-width: 0;
}
.backup-item-date {
font-size: 0.875rem;
font-weight: 500;
}
.backup-item-meta {
font-size: 0.75rem;
color: var(--admin-text-muted);
}
.backup-item-actions {
display: flex;
align-items: center;
gap: 0.375rem;
flex-shrink: 0;
}
.backup-item-confirm {
font-size: 0.8125rem;
color: var(--admin-text-muted);
margin-right: 0.25rem;
}
.backup-progress {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: var(--t-surface-raised);
border-radius: var(--radius-md);
}
.backup-progress-text {
font-weight: 500;
}
.backup-progress-hint {
font-size: 0.8125rem;
color: var(--admin-text-muted);
}
.backup-dropzone {
border: 2px dashed var(--t-border-subtle);
border-radius: var(--radius-md);
padding: 1.5rem;
text-align: center;
transition: border-color 0.15s, background-color 0.15s;
color: var(--admin-text-muted);
&:hover, &.phx-drop-target {
border-color: var(--t-primary);
background: oklch(from var(--t-primary) l c h / 0.05);
}
}
.backup-dropzone-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.backup-dropzone-link {
color: var(--t-primary);
cursor: pointer;
text-decoration: underline;
}
.backup-upload-entry {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.625rem 0.875rem;
background: var(--t-surface-raised);
border-radius: var(--radius-md);
margin-top: 0.75rem;
font-size: 0.875rem;
progress {
flex: 1;
height: 0.375rem;
border-radius: 9999px;
overflow: hidden;
background: var(--t-surface-inset);
&::-webkit-progress-bar {
background: var(--t-surface-inset);
}
&::-webkit-progress-value {
background: var(--t-primary);
}
&::-moz-progress-bar {
background: var(--t-primary);
}
}
}
.backup-comparison {
margin-top: 0.5rem;
}
.backup-comparison-grid {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0.75rem;
align-items: start;
margin-bottom: 1rem;
}
.backup-comparison-col {
padding: 0.75rem 1rem;
background: var(--t-surface-raised);
border-radius: var(--radius-md);
}
.backup-comparison-label {
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--admin-text-muted);
margin-bottom: 0.5rem;
}
.backup-comparison-arrow {
display: flex;
align-items: center;
justify-content: center;
padding-top: 1.5rem;
color: var(--admin-text-muted);
}
.backup-comparison-stats {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.8125rem;
> div {
display: flex;
justify-content: space-between;
gap: 1rem;
}
dt {
color: var(--admin-text-muted);
}
dd {
font-weight: 500;
font-variant-numeric: tabular-nums;
}
}
.backup-validation {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-radius: var(--radius-md);
font-size: 0.8125rem;
margin-bottom: 0.75rem;
}
.backup-validation-ok {
background: oklch(0.95 0.1 145);
color: oklch(0.35 0.15 145);
}
.backup-validation-error {
background: oklch(0.95 0.05 25);
color: oklch(0.35 0.1 25);
}
.backup-warning {
padding: 0.75rem 1rem;
background: oklch(0.96 0.03 60);
border-radius: var(--radius-md);
font-size: 0.875rem;
color: oklch(0.35 0.1 60);
p {
margin-bottom: 0.75rem;
}
}
} /* @layer admin */

View File

@@ -14,6 +14,18 @@
height: 1.5rem;
}
.hero-arrow-down-tray-mini {
--hero-arrow-down-tray-mini: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20viewBox="0%200%2020%2020"%20fill="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20d="M10.75%202.75a.75.75%200%200%200-1.5%200v8.614L6.295%208.235a.75.75%200%201%200-1.09%201.03l4.25%204.5a.75.75%200%200%200%201.09%200l4.25-4.5a.75.75%200%200%200-1.09-1.03l-2.955%203.129V2.75Z"/>%20%20<path%20d="M3.5%2012.75a.75.75%200%200%200-1.5%200v2.5A2.75%202.75%200%200%200%204.75%2018h10.5A2.75%202.75%200%200%200%2018%2015.25v-2.5a.75.75%200%200%200-1.5%200v2.5c0%20.69-.56%201.25-1.25%201.25H4.75c-.69%200-1.25-.56-1.25-1.25v-2.5Z"/></svg>');
-webkit-mask: var(--hero-arrow-down-tray-mini);
mask: var(--hero-arrow-down-tray-mini);
mask-repeat: no-repeat;
background-color: currentColor;
vertical-align: middle;
display: inline-block;
width: 1.25rem;
height: 1.25rem;
}
.hero-arrow-left {
--hero-arrow-left: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M10.5%2019.5%203%2012m0%200%207.5-7.5M3%2012h18"/></svg>');
-webkit-mask: var(--hero-arrow-left);
@@ -62,6 +74,18 @@
height: 1.25rem;
}
.hero-arrow-right {
--hero-arrow-right: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M13.5%204.5%2021%2012m0%200-7.5%207.5M21%2012H3"/></svg>');
-webkit-mask: var(--hero-arrow-right);
mask: var(--hero-arrow-right);
mask-repeat: no-repeat;
background-color: currentColor;
vertical-align: middle;
display: inline-block;
width: 1.5rem;
height: 1.5rem;
}
.hero-arrow-right-start-on-rectangle {
--hero-arrow-right-start-on-rectangle: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M15.75%209V5.25A2.25%202.25%200%200%200%2013.5%203h-6a2.25%202.25%200%200%200-2.25%202.25v13.5A2.25%202.25%200%200%200%207.5%2021h6a2.25%202.25%200%200%200%202.25-2.25V15m3%200%203-3m0%200-3-3m3%203H9"/></svg>');
-webkit-mask: var(--hero-arrow-right-start-on-rectangle);
@@ -110,6 +134,18 @@
height: 1.5rem;
}
.hero-arrow-up-tray-mini {
--hero-arrow-up-tray-mini: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20viewBox="0%200%2020%2020"%20fill="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20d="M9.25%2013.25a.75.75%200%200%200%201.5%200V4.636l2.955%203.129a.75.75%200%200%200%201.09-1.03l-4.25-4.5a.75.75%200%200%200-1.09%200l-4.25%204.5a.75.75%200%201%200%201.09%201.03L9.25%204.636v8.614Z"/>%20%20<path%20d="M3.5%2012.75a.75.75%200%200%200-1.5%200v2.5A2.75%202.75%200%200%200%204.75%2018h10.5A2.75%202.75%200%200%200%2018%2015.25v-2.5a.75.75%200%200%200-1.5%200v2.5c0%20.69-.56%201.25-1.25%201.25H4.75c-.69%200-1.25-.56-1.25-1.25v-2.5Z"/></svg>');
-webkit-mask: var(--hero-arrow-up-tray-mini);
mask: var(--hero-arrow-up-tray-mini);
mask-repeat: no-repeat;
background-color: currentColor;
vertical-align: middle;
display: inline-block;
width: 1.25rem;
height: 1.25rem;
}
.hero-arrow-uturn-left {
--hero-arrow-uturn-left: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M9%2015%203%209m0%200%206-6M3%209h12a6%206%200%200%201%200%2012h-3"/></svg>');
-webkit-mask: var(--hero-arrow-uturn-left);
@@ -386,6 +422,18 @@
height: 1.25rem;
}
.hero-circle-stack {
--hero-circle-stack: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M20.25%206.375c0%202.278-3.694%204.125-8.25%204.125S3.75%208.653%203.75%206.375m16.5%200c0-2.278-3.694-4.125-8.25-4.125S3.75%204.097%203.75%206.375m16.5%200v11.25c0%202.278-3.694%204.125-8.25%204.125s-8.25-1.847-8.25-4.125V6.375m16.5%200v3.75m-16.5-3.75v3.75m16.5%200v3.75C20.25%2016.153%2016.556%2018%2012%2018s-8.25-1.847-8.25-4.125v-3.75m16.5%200c0%202.278-3.694%204.125-8.25%204.125s-8.25-1.847-8.25-4.125"/></svg>');
-webkit-mask: var(--hero-circle-stack);
mask: var(--hero-circle-stack);
mask-repeat: no-repeat;
background-color: currentColor;
vertical-align: middle;
display: inline-block;
width: 1.5rem;
height: 1.5rem;
}
.hero-clipboard {
--hero-clipboard: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M15.666%203.888A2.25%202.25%200%200%200%2013.5%202.25h-3c-1.03%200-1.9.693-2.166%201.638m7.332%200c.055.194.084.4.084.612v0a.75.75%200%200%201-.75.75H9a.75.75%200%200%201-.75-.75v0c0-.212.03-.418.084-.612m7.332%200c.646.049%201.288.11%201.927.184%201.1.128%201.907%201.077%201.907%202.185V19.5a2.25%202.25%200%200%201-2.25%202.25H6.75A2.25%202.25%200%200%201%204.5%2019.5V6.257c0-1.108.806-2.057%201.907-2.185a48.208%2048.208%200%200%201%201.927-.184"/></svg>');
-webkit-mask: var(--hero-clipboard);
@@ -506,6 +554,18 @@
height: 1rem;
}
.hero-credit-card {
--hero-credit-card: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M2.25%208.25h19.5M2.25%209h19.5m-16.5%205.25h6m-6%202.25h3m-3.75%203h15a2.25%202.25%200%200%200%202.25-2.25V6.75A2.25%202.25%200%200%200%2019.5%204.5h-15a2.25%202.25%200%200%200-2.25%202.25v10.5A2.25%202.25%200%200%200%204.5%2019.5Z"/></svg>');
-webkit-mask: var(--hero-credit-card);
mask: var(--hero-credit-card);
mask-repeat: no-repeat;
background-color: currentColor;
vertical-align: middle;
display: inline-block;
width: 1.5rem;
height: 1.5rem;
}
.hero-cube {
--hero-cube: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="m21%207.5-9-5.25L3%207.5m18%200-9%205.25m9-5.25v9l-9%205.25M3%207.5l9%205.25M3%207.5v9l9%205.25m0-9v9"/></svg>');
-webkit-mask: var(--hero-cube);
@@ -602,6 +662,18 @@
height: 1.5rem;
}
.hero-exclamation-circle-mini {
--hero-exclamation-circle-mini: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20viewBox="0%200%2020%2020"%20fill="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20fill-rule="evenodd"%20d="M18%2010a8%208%200%201%201-16%200%208%208%200%200%201%2016%200Zm-8-5a.75.75%200%200%201%20.75.75v4.5a.75.75%200%200%201-1.5%200v-4.5A.75.75%200%200%201%2010%205Zm0%2010a1%201%200%201%200%200-2%201%201%200%200%200%200%202Z"%20clip-rule="evenodd"/></svg>');
-webkit-mask: var(--hero-exclamation-circle-mini);
mask: var(--hero-exclamation-circle-mini);
mask-repeat: no-repeat;
background-color: currentColor;
vertical-align: middle;
display: inline-block;
width: 1.25rem;
height: 1.25rem;
}
.hero-exclamation-triangle {
--hero-exclamation-triangle: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M12%209v3.75m-9.303%203.376c-.866%201.5.217%203.374%201.948%203.374h14.71c1.73%200%202.813-1.874%201.948-3.374L13.949%203.378c-.866-1.5-3.032-1.5-3.898%200L2.697%2016.126ZM12%2015.75h.007v.008H12v-.008Z"/></svg>');
-webkit-mask: var(--hero-exclamation-triangle);
@@ -758,6 +830,30 @@
height: 1.5rem;
}
.hero-lock-closed-mini {
--hero-lock-closed-mini: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20viewBox="0%200%2020%2020"%20fill="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20fill-rule="evenodd"%20d="M10%201a4.5%204.5%200%200%200-4.5%204.5V9H5a2%202%200%200%200-2%202v6a2%202%200%200%200%202%202h10a2%202%200%200%200%202-2v-6a2%202%200%200%200-2-2h-.5V5.5A4.5%204.5%200%200%200%2010%201Zm3%208V5.5a3%203%200%201%200-6%200V9h6Z"%20clip-rule="evenodd"/></svg>');
-webkit-mask: var(--hero-lock-closed-mini);
mask: var(--hero-lock-closed-mini);
mask-repeat: no-repeat;
background-color: currentColor;
vertical-align: middle;
display: inline-block;
width: 1.25rem;
height: 1.25rem;
}
.hero-lock-open-mini {
--hero-lock-open-mini: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20viewBox="0%200%2020%2020"%20fill="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20fill-rule="evenodd"%20d="M14.5%201A4.5%204.5%200%200%200%2010%205.5V9H3a2%202%200%200%200-2%202v6a2%202%200%200%200%202%202h10a2%202%200%200%200%202-2v-6a2%202%200%200%200-2-2h-1.5V5.5a3%203%200%201%201%206%200v2.75a.75.75%200%200%200%201.5%200V5.5A4.5%204.5%200%200%200%2014.5%201Z"%20clip-rule="evenodd"/></svg>');
-webkit-mask: var(--hero-lock-open-mini);
mask: var(--hero-lock-open-mini);
mask-repeat: no-repeat;
background-color: currentColor;
vertical-align: middle;
display: inline-block;
width: 1.25rem;
height: 1.25rem;
}
.hero-magnifying-glass {
--hero-magnifying-glass: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="m21%2021-5.197-5.197m0%200A7.5%207.5%200%201%200%205.196%205.196a7.5%207.5%200%200%200%2010.607%2010.607Z"/></svg>');
-webkit-mask: var(--hero-magnifying-glass);