m/paliad#77 Slice A. Folds the unbuilt t-paliad-214 Slice 3 (org async export) into a new "Backup Mode" surface gated by adminGate. m's calls (all 4 material picks per design §2): - Storage: local disk PALIAD_EXPORT_DIR (LocalDiskStore only) - Format: .zip bundle (xlsx + JSON + CSV + README) — no-lock-in preserved - paliadin_turns + paliadin_aichat_conversation: EXCLUDE structurally - Scheduler (Slice B): nightly 03:00 UTC, env-tunable Wiring: - mig 123 adds paliad.backups catalog table (kind/status/storage_uri/ size/row_counts/warnings/error/deleted_at + admin-only RLS). - ExportService.WriteOrg + orgSheetQueries enumerate 37 entity sheets + 12 ref sheets; REPEATABLE READ READ ONLY tx wraps the dump for snapshot consistency (design §3.3). - writeBundle + runSheetQuery refactored to take a sqlx.QueryerContext so both *sqlx.DB (personal/project paths, unchanged) and *sqlx.Tx (org snapshot path) work. - BackupRunner orchestrates: catalog INSERT → audit INSERT (event_type='backup_created') → WriteOrg → ArtifactStore.Put → patch catalog + audit on success/failure. - ArtifactStore interface + LocalDiskStore impl (defense-in-depth key validation + URI-outside-dir guard). - Sentinel actor for scheduled runs: actor_email='system@paliad', actor_id=NULL — no phantom user in paliad.users. - Admin handlers POST /api/admin/backups/run + GET list/get/download behind adminGate(users, …); /admin/backups page + sidebar entry + bilingual i18n keys. - BackupRunner only wired when PALIAD_EXPORT_DIR is set; routes return 503 otherwise (same shape as requireDB). Tests: 8 pure-function tests cover registry shape (no dups, paliadin absent both as sheet name and SQL substring, ref__* sheets unscoped, every sheet has ORDER BY) and LocalDiskStore (round-trip, bad-key rejection, URI-traversal rejection, mkdir on construction). go build ./... + go test ./internal/... clean. bun run build clean. Slice B (BackupScheduler + retention cleanup) and Slice C (UI polish) are separate follow-ups per head's instruction.
97 lines
4.0 KiB
TypeScript
97 lines
4.0 KiB
TypeScript
import { h } from "./jsx";
|
|
import { Sidebar } from "./components/Sidebar";
|
|
import { PaliadinWidget } from "./components/PaliadinWidget";
|
|
import { BottomNav } from "./components/BottomNav";
|
|
import { Footer } from "./components/Footer";
|
|
import { PWAHead } from "./components/PWAHead";
|
|
|
|
// Backup Mode admin page (t-paliad-246 / m/paliad#77 Slice A).
|
|
//
|
|
// global_admin only — gated by adminGate(...) in handlers.go. Shows the
|
|
// chronological list of backup runs (one row per kind in
|
|
// {scheduled, on_demand}) plus a button to kick off an on-demand backup.
|
|
// Catalog rows + the "run now" action are fetched client-side via
|
|
// /api/admin/backups.
|
|
export function renderAdminBackups(): string {
|
|
return "<!DOCTYPE html>" + (
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
<meta name="theme-color" content="#BFF355" />
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
|
<PWAHead />
|
|
<title data-i18n="admin.backups.title">Backups — Paliad</title>
|
|
<link rel="stylesheet" href="/assets/global.css" />
|
|
</head>
|
|
<body className="has-sidebar">
|
|
<Sidebar currentPath="/admin/backups" />
|
|
<BottomNav currentPath="/admin/backups" />
|
|
|
|
<main>
|
|
<section className="tool-page">
|
|
<div className="container">
|
|
<div className="tool-header">
|
|
<div>
|
|
<h1 data-i18n="admin.backups.heading">Backups</h1>
|
|
<p className="tool-subtitle" data-i18n="admin.backups.subtitle">
|
|
Vollständige Snapshots aller Daten — manuell oder zeitgesteuert.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<button
|
|
className="btn-primary"
|
|
id="admin-backups-run-btn"
|
|
type="button"
|
|
data-i18n="admin.backups.run_now"
|
|
>
|
|
Backup jetzt erstellen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="admin-backups-feedback" className="form-msg" style="display:none" />
|
|
|
|
<div className="entity-table-wrap">
|
|
<table className="entity-table entity-table--readonly">
|
|
<thead>
|
|
<tr>
|
|
<th data-i18n="admin.backups.col.started">Erstellt</th>
|
|
<th data-i18n="admin.backups.col.kind">Auslöser</th>
|
|
<th data-i18n="admin.backups.col.status">Status</th>
|
|
<th data-i18n="admin.backups.col.requested_by">Angefordert von</th>
|
|
<th data-i18n="admin.backups.col.size">Größe</th>
|
|
<th data-i18n="admin.backups.col.rows">Zeilen</th>
|
|
<th data-i18n="admin.backups.col.actions">Aktion</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="admin-backups-tbody">
|
|
<tr>
|
|
<td colspan={7} data-i18n="admin.backups.loading">Lade …</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="entity-empty" id="admin-backups-empty" style="display:none">
|
|
<p data-i18n="admin.backups.empty">Noch keine Backups vorhanden.</p>
|
|
</div>
|
|
|
|
<p className="tool-footer-note" id="admin-backups-footer">
|
|
<span data-i18n="admin.backups.footer.note">
|
|
Geplante Backups werden in einer späteren Slice aktiviert. Manuelle Backups stehen jetzt zur Verfügung.
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<Footer />
|
|
<PaliadinWidget />
|
|
<script src="/assets/admin-backups.js"></script>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|