librelad 2e7ab3235a ux(backup): next-run hint in the Backup status card header
The Backup status card sat with just a heading + tooltip on the right;
the Locations card on the same row already had a hint pill ("Active
destinations"). Mirror that pattern: show the next scheduled backup
time pushed to the right of the heading, so the user can see at a
glance when the daily run will fire without digging into Configuration.

Derived purely client-side from CFG_BACKUP_CRONTAB_APP (read off the
already-loaded window.systemConfigs map) — no backend surface needed:

  - nextCronFireTime(expr) parses a 5-field crontab (minute hour dom
    month dow) supporting *, N, lists (N,M,O), ranges (N-M), and
    steps (* /N, N-M/S). Walks one minute at a time from now+1, honours
    the POSIX OR rule for DOM+DOW, caps at 366 days so an unmatchable
    expression doesn't loop forever, returns null on bad syntax so the
    UI falls back gracefully.
  - formatRelativeFuture(when) — formatRelative's future-tense sibling:
    "in 6h", "tomorrow", "in 3d".
  - formatScheduleClock(when) — "at 05:00" today, "Mon 05:00" otherwise.

Hint slot rendered in #backup-next-run. Three states:
  - parseable + computable        "Next backup tomorrow · at 05:00"
                                  + title with absolute time + schedule
  - unparseable schedule          "Schedule: <raw>"  with title hint
  - empty CFG_BACKUP_CRONTAB_APP  "No schedule set"  with title hint

Smoke-tested the cron parser against "0 5 * * *", "*/15 * * * *",
"30 23 * * 0", "0 0 1 * *", "", "garbage", and "0 5 * *" (4 fields).

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 01:50:49 +01:00

274 lines
14 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

<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>Backup status <span class="tooltip" title="Latest backup per app + System config. Back up System first — it's needed to restore the rest." style="font-size:.75em;opacity:.7;cursor:help"></span></h2>
<span class="backup-card-hint" id="backup-next-run" title="Next scheduled backup run (from the app backup crontab)"></span>
</div>
<!-- The "System config" tile is rendered FIRST inside this grid
by renderDashboard() / renderSystemTile(), styled the same
as an app tile but with a server icon and two inline action
buttons (back up / restore). Folding both into one grid
gives a single at-a-glance checklist instead of two
parallel sections. -->
<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>
</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>Cross-host migrate <span class="tooltip" title="Pulls a backup taken on another host out of a shared backup location and lays it down here. The destination's existing copy of the app is backed up first (rollback safety), then replaced." style="font-size:.75em;opacity:.7;cursor:help"></span></h2>
<span class="backup-card-hint">Restore an app from another LibrePortal</span>
</div>
<div class="backup-migrate-empty" id="backup-migrate-empty" hidden>
<!-- Bordered callout panel — matches the per-app task-card
visual so the empty state reads as a contained block
rather than floating centred text. -->
<div class="backup-empty-state" style="border: 1px solid var(--border-color, #2a2a2a); background: var(--surface-2, rgba(255,255,255,0.02)); border-radius: 10px; padding: 28px 24px; margin: 12px 0;">
<div style="margin-bottom: 6px; opacity: .7; display: flex; justify-content: center;">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<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>
</div>
<div style="margin-bottom: 14px;">
No backups from other hosts visible in any enabled location.<br>
Add a <strong>shared backup location</strong> on both hosts to enable cross-host migrate.
</div>
<button type="button" class="backup-primary-btn" data-action="go-to-locations">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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>
Open Locations
</button>
</div>
</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-pick-modal">
<div class="backup-modal-inner">
<div class="backup-modal-header">
<h3>Back up</h3>
<button class="backup-modal-close" data-close-modal>×</button>
</div>
<div class="backup-modal-body" id="backup-pick-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-pick-confirm">Back up selected</button>
</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-delete-location-modal">
<div class="backup-modal-inner">
<div class="backup-modal-header">
<h3>Delete backup location</h3>
<button class="backup-modal-close" data-close-modal>×</button>
</div>
<div class="backup-modal-body" id="backup-delete-location-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-location-confirm">Delete location</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>