Phase 1 of the migration-system refresh. Surfaces Phase 0's kernel
(libreportal restore migrate ...) as a WebUI flow so users don't have
to drop to the CLI to pull an app from a peer's backups.
backend / data generator:
scripts/webui/data/generators/backup/webui_backup_migrate.sh
Walks every enabled backup location, lists every (other_host, app)
pair with snapshot count + latest id/date, and emits a single
destination summary block (installed apps, running apps, disk free)
so the frontend can compute collisions and warnings without per-row
API round-trips. Filters out our own hostname — we don't migrate to
ourselves. Output: data/backup/generated/migrate.json.
Hooked into the standard webuiLibrePortalUpdate refresh pipeline,
so 'libreportal regen webui' (and the periodic task-processor poll)
keep it fresh on their own.
frontend:
- New 'Migrate' sidebar tab on /backup, sits between Locations and
Configuration. Path-based URL: /backup/migrate.
- Per-source-host cards listing every available app, with snapshot
count + relative-time hint, collision dot when the app is already
installed here, and per-app + per-host migrate buttons.
- Confirm modal with two checkboxes matching the kernel's defaults:
[✓] Back up the destination's existing copy first (pre-migrate
backup; auto-disabled when there's nothing to back up)
[✓] Rewrite host-bound URLs to this host (URL rewrite
— uncheck only to keep source hostnames)
On confirm, runs 'libreportal restore migrate app/system …' via the
task system; opt-out checkboxes append --no-pre-backup / --keep-urls
only when the user un-ticks, matching the kernel's default-on flags.
- Empty state when no other hosts have visible backups, explaining
the shared-backup-location prerequisite.
The CLI dispatcher hooks (Phase 0) wire restore migrate app/system to
migrateApplyApp/migrateApplySystem, so the WebUI gets pre-backup safety,
URL rewrite, and structured progress (when --json-progress is set; not
needed here yet — the task system's log tail is enough for v1).
Signed-off-by: librelad <librelad@digitalangels.vip>
253 lines
12 KiB
HTML
253 lines
12 KiB
HTML
<div class="container backup-layout">
|
||
<div class="mobile-overlay" id="mobile-overlay"></div>
|
||
|
||
<div class="sidebar" id="sidebar">
|
||
<div class="sidebar-section" id="backup-sidebar-list">
|
||
<div class="category active" data-backup-tab="dashboard">
|
||
<svg class="category-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="3" y="3" width="7" height="9"></rect>
|
||
<rect x="14" y="3" width="7" height="5"></rect>
|
||
<rect x="14" y="12" width="7" height="9"></rect>
|
||
<rect x="3" y="16" width="7" height="5"></rect>
|
||
</svg>
|
||
Dashboard
|
||
</div>
|
||
<div class="category" data-backup-tab="backups">
|
||
<svg class="category-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||
<polyline points="17 8 12 3 7 8"></polyline>
|
||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||
</svg>
|
||
Backups
|
||
</div>
|
||
<div class="category" data-backup-tab="locations">
|
||
<svg class="category-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
|
||
<circle cx="12" cy="10" r="3"></circle>
|
||
</svg>
|
||
Locations
|
||
</div>
|
||
<div class="category" data-backup-tab="migrate">
|
||
<svg class="category-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M3 12h18"></path>
|
||
<polyline points="12 5 19 12 12 19"></polyline>
|
||
<path d="M3 5v14"></path>
|
||
</svg>
|
||
Migrate
|
||
</div>
|
||
<div class="category" data-backup-tab="configuration">
|
||
<svg class="category-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="12" cy="12" r="3"></circle>
|
||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||
</svg>
|
||
Configuration
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main">
|
||
<div class="backup-page" id="backup-page">
|
||
<div class="config-section backup-page-section">
|
||
<div class="page-header" id="backup-page-header">
|
||
<div class="page-header-icon-slot" id="backup-page-header-icon"></div>
|
||
<div class="page-header-title">
|
||
<h1 id="backup-section-title">Dashboard</h1>
|
||
<p id="backup-section-subtitle"></p>
|
||
</div>
|
||
<div class="page-header-actions">
|
||
<button class="backup-refresh-btn" id="backup-refresh-btn" title="Refresh">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="23 4 23 10 17 10"></polyline>
|
||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||
</svg>
|
||
Refresh
|
||
</button>
|
||
<button class="backup-primary-btn" id="backup-primary-action"></button>
|
||
<div class="backup-export-menu" id="backup-export-menu" role="menu" hidden>
|
||
<button type="button" class="backup-export-menu-item" data-action="export-passwords" role="menuitem">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||
<polyline points="7 10 12 15 17 10"></polyline>
|
||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||
</svg>
|
||
Repository Passwords
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="backup-page-body">
|
||
<span class="backup-engine-badge" id="backup-engine-badge" hidden>restic</span>
|
||
|
||
<section class="backup-tabpanel active" id="backup-panel-dashboard">
|
||
<div class="backup-summary-row" id="backup-summary-row"></div>
|
||
<div class="backup-cards-row">
|
||
<div class="backup-card">
|
||
<div class="backup-card-header">
|
||
<h2>Per-app status</h2>
|
||
<span class="backup-card-hint">Latest backup per app on this host</span>
|
||
</div>
|
||
<div class="backup-app-grid" id="backup-app-grid"></div>
|
||
</div>
|
||
<div class="backup-card">
|
||
<div class="backup-card-header">
|
||
<h2>Locations</h2>
|
||
<span class="backup-card-hint">Active destinations</span>
|
||
</div>
|
||
<div class="backup-repo-list" id="backup-repo-list-summary"></div>
|
||
</div>
|
||
</div>
|
||
<div class="backup-card backup-system-card">
|
||
<div class="backup-card-header">
|
||
<h2>
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:.7;vertical-align:-3px;margin-right:7px">
|
||
<rect x="2" y="3" width="20" height="6" rx="1"></rect>
|
||
<rect x="2" y="9" width="20" height="6" rx="1"></rect>
|
||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||
<line x1="6" y1="12" x2="6.01" y2="12"></line>
|
||
</svg>System config
|
||
</h2>
|
||
<span class="backup-card-hint">Global settings, WebUI login & backup-location credentials</span>
|
||
</div>
|
||
<div class="backup-app-tile-meta" id="backup-system-status" style="margin:0 0 10px">
|
||
<span class="backup-status-dot none"></span><span>No backup yet</span>
|
||
</div>
|
||
<p class="backup-card-hint" style="margin:0 0 12px">
|
||
Snapshot the LibrePortal system config to every enabled location so a bare-metal
|
||
restore is self-sufficient — without it, the credentials needed to reach your own
|
||
backups live only on this box. Runs automatically with “Backup all apps” too.
|
||
</p>
|
||
<div class="backup-system-actions" style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button type="button" class="backup-primary-btn" data-action="backup-system">Back up now</button>
|
||
<button type="button" class="backup-secondary-btn" data-action="restore-system">Restore…</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="backup-tabpanel" id="backup-panel-backups">
|
||
<div class="backup-filters">
|
||
<input class="backup-filter-input" id="backup-snapshot-filter" placeholder="Filter by app, host, or backup id…">
|
||
<select class="backup-filter-select form-control" id="backup-snapshot-repo">
|
||
<option value="">All locations</option>
|
||
</select>
|
||
</div>
|
||
<div class="backup-snapshot-table-wrap">
|
||
<table class="backup-snapshot-table">
|
||
<thead>
|
||
<tr>
|
||
<th>App</th>
|
||
<th>Host</th>
|
||
<th>Location</th>
|
||
<th>When</th>
|
||
<th>ID</th>
|
||
<th class="backup-col-actions">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="backup-snapshot-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="backup-tabpanel" id="backup-panel-locations">
|
||
<div class="backup-location-list" id="backup-location-list"></div>
|
||
</section>
|
||
|
||
<section class="backup-tabpanel" id="backup-panel-migrate">
|
||
<div class="backup-card backup-migrate-card">
|
||
<div class="backup-card-header">
|
||
<h2>Restore an app from another LibrePortal</h2>
|
||
<span class="backup-card-hint">
|
||
Pulls a snapshot taken on another host out of a shared backup location and
|
||
lays it down here. The destination's existing copy of the app is snapshotted
|
||
first (rollback safety), then replaced.
|
||
</span>
|
||
</div>
|
||
<div class="backup-migrate-empty" id="backup-migrate-empty" hidden>
|
||
<p>No backups from other hosts found in any enabled location.</p>
|
||
<p class="backup-card-hint">
|
||
Either no other LibrePortal has backed up to a location this host can see,
|
||
or this is the only host using its locations. Add a shared backup location
|
||
on both hosts to enable cross-host migrate.
|
||
</p>
|
||
</div>
|
||
<div class="backup-migrate-body" id="backup-migrate-body"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="backup-tabpanel" id="backup-panel-configuration">
|
||
<div id="backup-configuration-body"></div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="backup-modal" id="backup-restore-modal">
|
||
<div class="backup-modal-inner">
|
||
<div class="backup-modal-header">
|
||
<h3>Restore backup</h3>
|
||
<button class="backup-modal-close" data-close-modal>×</button>
|
||
</div>
|
||
<div class="backup-modal-body" id="backup-restore-modal-body"></div>
|
||
<div class="backup-modal-footer">
|
||
<button class="backup-secondary-btn" data-close-modal>Cancel</button>
|
||
<button class="backup-primary-btn" id="backup-restore-confirm">Restore</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="backup-modal" id="backup-delete-modal">
|
||
<div class="backup-modal-inner">
|
||
<div class="backup-modal-header">
|
||
<h3>Delete backup</h3>
|
||
<button class="backup-modal-close" data-close-modal>×</button>
|
||
</div>
|
||
<div class="backup-modal-body" id="backup-delete-modal-body"></div>
|
||
<div class="backup-modal-footer">
|
||
<button class="backup-secondary-btn" data-close-modal>Cancel</button>
|
||
<button class="backup-danger-btn" id="backup-delete-confirm">Delete</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="backup-modal" id="backup-migrate-modal">
|
||
<div class="backup-modal-inner">
|
||
<div class="backup-modal-header">
|
||
<h3>Migrate from another LibrePortal</h3>
|
||
<button class="backup-modal-close" data-close-modal>×</button>
|
||
</div>
|
||
<div class="backup-modal-body" id="backup-migrate-modal-body"></div>
|
||
<div class="backup-modal-footer">
|
||
<button class="backup-secondary-btn" data-close-modal>Cancel</button>
|
||
<button class="backup-primary-btn" id="backup-migrate-confirm">Start migrate</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="backup-modal" id="backup-engine-modal">
|
||
<div class="backup-modal-inner backup-modal-wide">
|
||
<div class="backup-modal-header">
|
||
<h3 id="backup-engine-modal-title">Backup engine details</h3>
|
||
<button class="backup-modal-close" data-close-modal>×</button>
|
||
</div>
|
||
<div class="backup-modal-body" id="backup-engine-modal-body"></div>
|
||
<div class="backup-modal-footer">
|
||
<button class="backup-secondary-btn" data-close-modal>Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="backup-modal" id="backup-add-location-modal">
|
||
<div class="backup-modal-inner">
|
||
<div class="backup-modal-header">
|
||
<h3>Add a backup location</h3>
|
||
<button class="backup-modal-close" data-close-modal>×</button>
|
||
</div>
|
||
<div class="backup-modal-body" id="backup-add-location-modal-body"></div>
|
||
<div class="backup-modal-footer">
|
||
<button class="backup-secondary-btn" data-close-modal>Cancel</button>
|
||
<button class="backup-primary-btn" id="backup-add-location-confirm">Add location</button>
|
||
</div>
|
||
</div>
|
||
</div>
|