40 Commits

Author SHA1 Message Date
librelad
bae9a79158 feat(webui): central task-refresh registry + close stale-UI gaps
Post-task UI refresh was scattered: every page added its own taskCompleted
listener and hard-coded which actions it cared about, so it was easy to add a
task and forget the refresh (stale UI), with no single place to see the wiring.

Adds TaskRefreshCoordinator (window.taskRefresh): one listener, with dedupe
(the SSE bus + synthetic fallbacks double-fire) and opt-in debounce (bursts
coalesce; per-task handlers run every time). Components now register a refresh
entry; window.taskRefresh.table() is the introspectable "what reloads when" map.

Migrated onto it: apps (install/uninstall/tool/config_update lifecycle +
restore/update/rebuild state), backups (backup/restore/delete), the update
badge, and the admin overview integrity badge. Gaps closed: restore/update/
rebuild now repaint app+service data. (start/stop/restart intentionally omitted —
no live status surface to refresh today; revisit if a running/stopped badge is
added. Storage reclaim/image-rm keep their own in-page refresh.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 22:06:39 +01:00
librelad
7ba281a390 ux(backup): redesign the snapshot details panel
The expanded snapshot detail reused the shared .task-meta/.meta-item
layout, which forces each field onto one nowrap line and clips long
values (the full date string, repo paths) mid-string. Give the backup
snapshot its own scoped label-over-value grid plus full-width Tags/Paths
blocks that wrap, surface app=/host=/engine= tags as their own fields,
and show a readable date (full timestamp on hover). Applied to both the
global Snapshots tab and the per-app Backups card so they match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 18:38:28 +01:00
librelad
d448d34f67 feat(backup): auto-refresh the backup page when a backup/restore finishes
The page has no live feed, so a completed backup/restore wasn't reflected
until a manual Refresh or re-navigation. Subscribe to the TaskEventBus
'taskCompleted' event and repaint on backup/restore completions. Debounced
to coalesce the burst when several per-app tasks finish together; only the
mounted instance reacts and a stale listener removes itself. The Refresh
button stays as a manual pull.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 18:25:24 +01:00
librelad
7dbeb307c9 ux(backup): drop the "Backup all apps" header action on Dashboard and Migrate
The primary header button defaulted to "Backup all apps" on every tab that
wasn't Locations/Configuration, so it showed on Dashboard and Migrate where
it isn't wanted (Dashboard backs up via the status-grid tiles; Migrate is
about moving hosts, not backing up). Keep it on the Backups tab and hide the
header action entirely on Dashboard and Migrate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 18:20:21 +01:00
librelad
ed319b0f94 fix(backup): configs backup gets its own task identity, not "Backup All Apps"
System-config backups (libreportal backup system) carry no app slug, so the
notification descriptor resolved a blank subject + no icon, and a system-only
pick collapsed to `backup all` when no apps were installed. Give them the
LibrePortal icon + a "Configs" subject, add backup-system to the system-task
logo detection, and guard the whole-fleet collapse on having >=1 app. Rename
the visible subject from "System config" to "Configs" throughout the backup UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 14:49:18 +01:00
librelad
d8f585aada ux(backup): global Backups tab matches the per-app card pattern
The /backup → Backups tab was the last surface still rendering snapshots
as a plain HTML table — every other backup-related list had moved to the
.task-item card pattern shared with Services. Cohesion-only refactor:
both surfaces now look identical, with the global view adding the
fields the per-app view doesn't need.

HTML: drops <table class="backup-snapshot-table"> + its <tbody>,
replaces with a single <div id="backup-snapshot-list"
class="backup-snapshot-rows"> that the same .backup-snapshot-flash
deep-link highlight already targets.

renderSnapshots() now emits .task-item cards via the new
_renderSnapshotRow() helper. Each card carries:

  app icon · "12h ago" title · app-name chip (linked) · location pill
  · timestamp chip · short-ID chip       Restore · Delete · Details

Extras vs the per-app card:
  - App-name chip — global list isn't scoped to one app, so each row
    needs to name the app it belongs to. The chip is the deep-link to
    /app/<name>/backups?snapshot=<id> (replaces the dashed-underline
    "link" treatment on the old App / ID table cells).
  - Delete button alongside Restore — destructive cleanup lives on the
    global view, not on the per-app card.
  - "System config" rows (snapshots without an app=<slug> tag) get the
    LibrePortal icon and no app-link (no per-app page to open).

Detail panel (expanded via header / Details button) shows App, Backup
ID, Location, full timestamp, Host, Tags, Paths — the same shape as
the per-app version, plus Host (relevant on the global multi-host view).

Click delegation:
  - [data-action="toggle-snapshot-row"] on the header + Details button
    toggles .task-details-open
  - Restore / Delete buttons now stopPropagation so clicking them
    doesn't also toggle the panel
  - Existing [data-deep-link] handler is reused by the app-name chip

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 02:00:05 +01:00
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
librelad
989123322b copy(backup): user-facing "snapshot" → "backup" across the UI
"Snapshot" is restic's term and leaks the tool's vocabulary into the
WebUI. Users think in "backups" — the on-page label even says "Backups"
already; only the secondary copy still said "snapshot". Renames the
remaining user-visible mentions while leaving code identifiers, API
keys, data attributes, CSS class names, and the ?snapshot= deep-link
param untouched (those are internal contracts and changing them would
churn for no user-visible win).

Renamed surfaces:
  - Per-app Backups tab header:
      "Snapshots for <app>" → "Backups for <app>"
      "across all configured repositories" → "across all configured locations"
  - BackupAppCard:
      "No snapshots yet"   → "No backups yet"
      "No snapshots found" → "No backups found"
      "Showing the most recent 50 of N snapshots" → "...of N backups"
      ID-chip tooltip "Snapshot ID" → "Backup ID"
      Detail panel "Snapshot ID:" → "Backup ID:"
  - Backup retention preset descriptions (KEEP_LAST/DAILY/WEEKLY/MONTHLY/
    YEARLY) — "snapshot per day/week/..." → "backup per day/week/..."
  - Personal preset hint: "6 monthly snapshots" → "6 monthly backups"
  - Restore confirmation modal hint: "snapshot restored in place" →
    "backup restored in place"
  - Config-warning banner copy adjusted so it doesn't introduce
    "snapshots" as a noun
  - Retention "Keep last" input suffix: "snapshots" → "backups"
  - Cross-host migrate tooltip: "snapshot" → "backup"

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 00:42:08 +01:00
librelad
e86a65042a ux(backup): per-app snapshot list in Services-tab style + drill-down nav
Restores the per-app snapshot list (regressed during the backup-system
revamp) and rebuilds it on the same .task-item visual the Services tab
uses, so the two app-page tabs read as a matched pair. Wires the three-
level navigation the user asked for end-to-end:

  /backup                         global dashboard + snapshot table
   └─ click app tile           →  /app/<name>/backups
       └─ click any snapshot row   expands to detail in place
   └─ click App / ID cell      →  /app/<name>/backups?snapshot=<id>
                                  (auto-expands + scrolls + flashes)

Per-app Backups tab (BackupAppCard):
  - Snapshots render as task-item rows: app icon, "12h ago" title,
    location pill, full timestamp chip, short-ID monospace chip,
    Restore + Details actions.
  - Click the row header (or "Details") to toggle a .task-details panel
    showing snapshot ID, location, full timestamp, host, tags, and the
    paths the snapshot covers.
  - Shows up to the 50 most recent; >50 surfaces a hint to the global
    backup center for the full list.
  - flattenSnapshots() now carries hostname/tags/paths through so the
    detail panel has real content.

Cross-page navigation:
  - Dashboard app-tile click navigates to /app/<name>/backups instead of
    opening the pick-now modal. The pick-now action is preserved as an
    explicit "Back up" pill that appears top-right on hover/focus.
    System tile keeps the old modal click (no dedicated page yet).
  - Global Snapshots table — the App and ID cells are now SPA-routed
    links to /app/<name>/backups?snapshot=<id>. Snapshots without an
    app=<slug> tag (system backups) stay plain text. Routed via
    navigateToRoute so the SPA mounts in place instead of a full reload.

Deep-link mechanism:
  - BackupAppCard._honorSnapshotDeepLink reads ?snapshot=<id> on render,
    finds the matching .backup-snapshot-item, opens its details, scrolls
    it into view, and applies a brief .backup-snapshot-flash (animated
    box-shadow pulse) so the user's eye lands on it after the SPA jump.

CSS:
  - backup.css gains .backup-snapshot-rows, the location pill, the
    monospace ID chip, the tag chips, the deep-link flash keyframes,
    the tile "Back up" pill (.backup-app-tile-action — only visible on
    hover/focus to keep the dashboard calm at rest), and the dashed
    underline link style for the snapshot-table deep-link cells.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 00:15:17 +01:00
librelad
bbd4014f8c ui(backup): replace delete-location native confirm() with the backup-modal pattern
The inline "Delete location" action was the last spot on the Backup
page still using the native browser confirm() — the snapshot delete
already uses the styled backup-modal, so the location delete sat out
as the odd one. Adds a new #backup-delete-location-modal matching the
existing modal shell (header / body / backup-danger-btn footer),
swaps deleteInlineLocation() to open it instead of confirm(), and
wires the confirm button to a new confirmDeleteLocation() that does
the actual `libreportal backup location remove <idx>` task.

Behaviour is the same — confirm body text moves into the modal as a
muted hint paragraph using backup-card-hint, location name bolded
for scannability. expandedLocs cleanup also moves into the confirm
handler so the row collapses only when the user actually deletes.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 00:11:07 +01:00
librelad
5984869c7a copy(backup): tighter dashboard subtitle, action-led
Old: "Backup status — system config + every app — at a glance."
New: "Check what's protected — and when it last ran."

The em-dash chain was filler and "at a glance" was redundant on the
dashboard tab (which is the at-a-glance view). New copy leads with
what the admin is here to do.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 19:13:41 +01:00
librelad
14bc0c3386 ui(backup): tile-click → Back-up checklist modal; LibrePortal icon on System tile; 2-up grid
Reshape the dashboard's Backup status grid into a click-to-pick UI:

- Removed the inline Back-up / Restore buttons from the System config
  tile. Same shape as an app tile now; LibrePortal app icon instead of
  the server-stack glyph.
- Grid is 2 columns (was auto-fill min 220px). Tiles are wider, read
  better, and the System tile no longer needs to span a full row to fit
  inline buttons.
- Click any tile (System or app) → opens a new "Back up" modal:
    * System config first (key=__system__, LibrePortal icon)
    * Every installed app, alphabetical
    * Checkbox per row + 'Select all' / 'Clear' shortcuts
    * The tile clicked is pre-ticked
- Confirm queues backup tasks:
    * Everything ticked  → single `libreportal backup all` (which also
      runs `backup system`) — one task instead of N
    * Subset            → one task per ticked item (`backup system`
      and/or `backup app create <slug>`)

Restore for System config used to live on the dashboard's inline
'Restore' button. It's now reachable via the Backups tab — system
snapshots appear in the snapshot list with the standard per-row
Restore action — same path apps already use. No new UI required;
just one fewer dashboard button.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 01:05:44 +01:00
librelad
102fc38da0 ui(backup): merge System config into the Backup status grid
The dashboard had two parallel sections — 'Per-app status' (every app's
latest backup) and a standalone 'System config' card below it. Folded
them into one grid: a single 'Backup status' card with the System config
tile rendered FIRST, then every app tile.

Why first: a bare-metal restore needs the system config (CFG_* +
backup-location credentials) — without it the backups exist but the
keys to reach them don't. Putting it at eye-level above the app tiles
makes the dependency visible.

System tile reuses the .backup-app-tile shape: server-stack icon,
'System config' as the name, status dot + 'Last backed up X ago' /
'No backup yet'. Plus two compact inline action buttons (Back up /
Restore) on the right that wire into the same data-action handlers
the old standalone card used — no behaviour change, just the visual
container.

grid-column: 1 / -1 on the system tile makes it span the row so the
two action buttons fit alongside the meta text without crushing the
app-tile grid template.

Section header: 'Per-app status' → 'Backup status' + hint 'System
config and every installed app's latest backup. System config always
first — a bare-metal restore needs it.' Dashboard subtitle updated
to match.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 00:42:58 +01:00
librelad
88b431ee86 style(migrate): tighten card header + give the empty state a real CTA
The Migrate tab carried two walls of explanation text — a 3-line hint
under the h2 ("Pulls a snapshot taken on another host…") and an even
longer empty-state paragraph ("Either no other LibrePortal has backed
up to a location this host can see, or this is the only host using its
locations…"). Both spelled out diagnosis the user can infer from the
empty list itself, and the tone didn't match the rest of the backup
page (cards elsewhere have a short title + a 4-6 word hint, with any
long explanation as a hover title attribute).

Three changes:

1. h2 down to "Cross-host migrate" with a small ℹ️ carrying the full
   explanation as a title= tooltip — matches the existing tooltip
   pattern in the Locations form (BACKUP_RETENTION_PRESET_META).
   The short subtitle "Restore an app from another LibrePortal" stays
   as backup-card-hint, mirroring "Per-app status / Latest backup per
   app on this host" elsewhere on the page.

2. The empty state is now the standard `<div class="backup-empty-state">`
   container (same shape Locations + Snapshots use), one trimmed line
   ("No backups from other hosts visible in any enabled location.
   Add a shared backup location on both hosts to enable cross-host
   migrate.") instead of two paragraphs.

3. Added an "Open Locations" CTA button inside the empty state — the
   #1 next-step for a user staring at this empty list is to add a
   shared location, which lives one tab over. New data-action
   "go-to-locations" wired through the existing event-delegation
   handler in backup-page.js calling switchTab('locations').

The renderMigrate JS still toggles #backup-migrate-empty.hidden — the
wrapper id is unchanged, only its inner markup tightened. No
behavioural change beyond the CTA + tab switch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 23:58:52 +01:00
librelad
82f64eb5c0 feat(migrate): app-specific hooks + peer friendly-name overlay (Phase 4)
Polish pass for the migration system. Two concrete additions; the live-mirror
and full drift-verify ideas from the original plan are intentionally
deferred — both need real-world test data to land correctly, and the kernel
already exposes everything they'd need.

Per-app migrate hooks (scripts/migrate/migrate_hooks.sh):
  Apps can declare two optional functions in their tools.sh (already
  auto-sourced per [[libreportal-modular-app-tools]]):

    <app>_migrate_pre()   — runs before stop+wipe
    <app>_migrate_post()  — runs after restart, before the user sees it

  Each receives:
    $1 = source identifier (peer name or backup-tag hostname)
    $2 = transport ("restic" | "direct-ssh")

  migrateRunHook() is now called from both migration apply paths:
    - migrate_apply.sh (restic-mediated, shared backup channel)
    - peer_pull.sh    (direct-SSH, peer-shell stream)

  Use cases: rotate federation keys after a Mastodon move, regenerate
  OIDC client secrets, drop SaaS-style locks, fix hostname-baked configs
  the URL-rewrite layer doesn't cover.

  Hooks are optional — apps without them inherit the standard flow.
  Failed hooks emit a non-fatal notice (the rest of the migrate still
  reaches 'done') so a single bad hook can't strand an otherwise-working
  app in stopped state.

Peer friendly-name overlay (Migrate tab):
  Was deferred from Phase 2 because it required Phase 3's UI to feel
  cohesive. BackupPage.refreshAll() now also fetches peers.json and builds
  a hostname → peer-name lookup. renderMigrate() shows
      'homelab (host: homelab.lan)'
  for any backup-channel peer that matches the source host, and falls back
  to the bare hostname when no peer is defined. Same data, friendlier UI.

Skipped (genuinely deferred, not just out of time):
  - Live mirror / warm-standby (continuous one-way sync). Needs a scheduler
    + drift-state to track. Right place for it is a separate feature on top
    of the existing kernel rather than bolted onto migrate.
  - Drift-verify ("what would change if I migrated?"). Cheap to write but
    needs a real cross-host pair to validate against — adding it untested
    would just be theatre.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 18:00:26 +01:00
librelad
52e4280a67 feat(webui): add 'Migrate' tab — restore an app from another LibrePortal
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>
2026-05-26 17:32:01 +01:00
librelad
3283b3f7a3 feat(webui): track system-config backup status on the dashboard
Make the system config a tracked backup, not just action buttons:

- engine: resticSystemSnapshotsJson (tag system=config) + engineSystemSnapshotsJson
  dispatcher — query the system snapshots the way per-app status is queried.
- webui_backup_dashboard.sh: emit a "system": { latest_snapshot, latest_time }
  object (latest system snapshot on the primary location), and exclude the
  libreportal WebUI app from the per-app grid (it's intentionally not backed up, so
  it no longer shows a perpetual "No backup yet" tile).
- backup dashboard card: a status line (dot + "Last backed up <relative>" / "No
  backup yet"), populated in renderDashboard from d.system — mirrors the app tiles.

Verified: shell + JS parse; dashboard content assembles to valid JSON with the
system key; engine query defined + dispatched; frontend reads d.system into the
#backup-system-status element.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 00:38:39 +01:00
librelad
c2c10103b8 feat(webui): surface system-config backup/restore on the backup dashboard
Add a "System config" card to the backup dashboard with two actions wired through
the task processor (same path as "Backup all apps"):

- "Back up now"  -> libreportal backup system
- "Restore…"     -> libreportal restore system  (confirm dialog explains it lands
  in a staging folder and never overwrites live config)

Card copy explains why it matters (the backup-location creds otherwise live only on
the box). Click handlers + runBackupSystem/confirmRestoreSystem added; JS parses,
data-actions match handlers, commands match the CLI subcommands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 00:31:23 +01:00
librelad
152d9c5d28 fix(webui): make all icon and data asset URLs absolute under path routing
Same class of bug as the topbar partial: icon and data-file references were
relative (icons/apps/x.svg, data/apps/...), so on deep path routes (/app/<name>,
/admin/config/x) the browser resolved them against the route dir and the SPA
catch-all served index.html with HTTP 200 instead of 404 — broken images and
silently-wrong JSON.

Make every reference absolute (anchored on the quote/backtick so already-absolute
/icons paths are untouched):
- JS: all icons/ and data/ literals + templates across components/utils/system
- html/topbar.html: logo <img>
- generators: webui_config.sh and webui_create_app_categories.sh now emit
  /icons/... into apps.json / apps-categories.json (regenerated on install)
- updated the two icon-path comments to match

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 23:20:42 +01:00
librelad
a103aa6864 refactor(webui): path-based URLs for apps, app, tasks, backup
Convert the remaining sections off the legacy ?= query form to clean paths,
matching the Admin area:
  /apps/<category>           (was /apps?=<category>)
  /app/<name>?tab=&task=     (was /app?=<name>&tab=&task=)
  /tasks/<category>?task=    (was /tasks?=<category>&task=)
  /backup/<tab>              (was /backup?=<tab>)

Builders updated everywhere (sidebar, dashboard, notifications, tasks, apps,
app tabs, task-actions, setup watcher); parsers now read the resource from the
path with the legacy ?= kept as a fallback so old links/bookmarks still work
(server already serves index.html at any depth). Route table gains /apps* and
orders it before /app* (since '/apps' startsWith '/app'); active-nav and
config/apps data-loading recognise the new paths.

Tab/task remain ordinary query params (modifiers, not the primary resource).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 19:03:54 +01:00
librelad
d3faa2514f feat(backup): SSH key card in the sftp location editor
When a location uses SSH key auth, show a key card: paste an existing private
key, or 'Generate keypair', then the card displays the public key to copy into
the remote server's authorized_keys (with Copy/Delete). Wires to the
ssh-key-set/generate/delete CLI; key mutations refresh locations.json so the
card reflects state immediately. applySshAuthVisibility toggles the card vs the
password field by auth mode. Private key only ever flows in (base64); only the
public key is ever shown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 16:17:34 +01:00
librelad
d6e7df8ada refactor(backup): move location field schema to a generated JSON
The per-type field map lived hardcoded in backup-page.js. Add a
webuiGenerateBackupSchema generator that emits the type -> ordered field list
to data/backup/generated/schema.json (wired into the backup regen chain and
the CLI 'webui generate backup'). The editor fetches it into this.locSchema
and reads it via locFieldsForType; BACKUP_LOC_FIELDS_BY_TYPE stays only as a
fallback if the fetch fails.

Keeps the data-in-generators pattern consistent — the schema now has one
backend source of truth. The dynamic show/hide behaviors (SSH auth, path
mode, engine filtering) remain frontend logic by nature.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 15:22:53 +01:00
librelad
1cc4b74b2e style(backup): show Type before Name in the location editor and add dialog
Type is the choice that determines which other fields appear, so it should be
the first thing you pick; Name is just a label. Reorder the Connection-tab
fields (and the Add-location dialog) to Type → Name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 15:14:47 +01:00
librelad
459609a35b style(backup): polish location tabs — drop stray descriptions, pad panels, round corners
- Remove the per-tab 'How LibrePortal connects…' description lines; the tab
  labels already say what each panel is, and the paragraphs read as misplaced
  titles.
- Give the tab panels even, comfortable padding (tabs-content padding zeroed so
  the panel owns it) instead of the cramped 2px sides.
- Round the tab strip's top corners (.tabs-list) so the strip + content read as
  one card — .tabs-content already rounds the bottom, leaving the top square.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 15:07:19 +01:00
librelad
d682178a08 feat(backup): configurable Default Backup Location; simplify Path Mode label
Automatic path mode hardcoded /docker/backups/<id>, baked into the Path Mode
dropdown label. Add a CFG_BACKUP_DEFAULT_PATH option in the Backup Engine
config ("Default Backup Location", default /docker/backups) and have
backupLocationResolvedPath build the auto path from it (<base>/<id>, trailing
slash tolerated). Defaults to the old path, so existing auto locations are
unchanged.

Path Mode's option is now just "Automatic" (no inline path); its tooltip
points at the Default Backup Location config option instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 14:51:43 +01:00
librelad
02e4f7d6ab style(backup): match location editor tabs to the app-detail tab design
Reuse the shared .tabs-wrapper/.tab-button/.tab-panel components (same as an
app's Config/Tasks tabs) for the location editor instead of bespoke tab CSS:
emoji + label buttons, equal-width strip, accent active state. Panels toggle
via the .active class like the rest of the UI; only the panel padding is
trimmed so it nests inside the backup row.

Also drop the now-dead 'No advanced options' empty state — every type has at
least Engine + append-only in the Advanced tab.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 14:46:03 +01:00
librelad
24abe412e0 feat(backup): move Engine into the location editor's Advanced tab
The backup engine is an implementation detail — LibrePortal picks a sensible
default and handles it — so it doesn't belong next to Name/Type on the
Connection tab. Add ENGINE to LOC_ADVANCED_SUFFIXES and mark it **ADVANCED**
in the location.config template + seed so it's metadata-driven.

Since the engine select now lives in the Advanced tab while SSH-auth and
path-mode stay on Connection, refreshInlineTypeFields re-applies the dynamic
behaviors (engine filtering, SSH/path visibility) against the shared
.task-details scope rather than a single panel.

Also fixed the live per-location engine label (restic -> Restic) which now
surfaces in the dropdown via the generator-emitted options.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 14:39:48 +01:00
librelad
6da8f80477 feat(backup): tabbed location editor (Connection / Retention / Advanced)
The expanded location row was one long form. Split it into tabs so it opens
showing only the Connection fields. Retention moves from a stacked section
into its own tab, and the advanced overrides (URI/SSH port/append-only) get
their own tab instead of the inline disclosure from the previous pass.

Field grouping is metadata-driven: locFieldGroups partitions a type's fields
into Connection vs Advanced via the configs.json "advanced" flag (with
LOC_ADVANCED_SUFFIXES as the legacy fallback). Type changes rebuild both the
Connection and Advanced panels since advanced fields are type-dependent too.
Save still reads every field across all panels (hidden tabs stay in the DOM).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 14:31:36 +01:00
librelad
c5ecc520aa feat(backup): system-driven location fields with an Advanced reveal
The Locations editor now renders field metadata from configs.json
(window.configData) instead of relying on the hardcoded BACKUP_LOC_FIELD_DEFS,
which drops to a fallback. Fields flagged advanced (URI override, SSH port,
append-only) move out of the main grid into a full-width "Advanced"
disclosure that's collapsed by default, so the common case stays simple.

Also load the unified config once on the backup page into window.configData
(metadata) + a flat window.systemConfigs (values). Previously systemConfigs
was only populated after a save — and with the full nested JSON, while the
code reads it as a flat map — so default-engine lookups and save-time change
detection silently misbehaved on first load. Both are now correct.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 13:44:41 +01:00
librelad
4e0b057277 feat(backup): capitalize Restic and surface the default engine in location dropdowns
- Display the restic engine as "Restic" to match BorgBackup/Kopia. The
  lowercase name lived in scripts/backup/engines/restic.json (drives the
  location-row engine pill, per-location engine select, and engine modal),
  the hardcoded per-location dropdown options, the engine-list fallback, and
  the config-option metadata. All set to "Restic".
- In each location's Engine dropdown, float the system-default engine
  (CFG_BACKUP_ENGINE) to the top and tag it "(default)", mirroring the
  retention-preset pattern.

Repo config metadata is the install template (add-only reconciliation), so
the live /docker/configs/backup/backup_engine label was updated in place too
for the global Configuration-tab dropdown on this install.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 13:16:33 +01:00
librelad
25027da86e style(backup): add icons to location buttons; move nebula CSS into theme folder
Buttons: the per-location Save changes / Delete location buttons had no icons,
unlike the apps-config action buttons. Add a save (floppy) icon and a trash
icon so they match the reference; colour comes from the nebula button groups
they already belong to.

Theme refactor: move the theme-specific [data-theme="nebula"] button/topbar/CTA
rules out of the shared css/themes.css and into themes/nebula/theme.css, where
the README says theme overrides belong. css/themes.css keeps only the generic,
non-theme-scoped defaults (solid status/accent buttons, danger-zone,
warning-banner) shared by dark-blue/light. No behaviour change: the nebula file
loads after css/themes.css so the moved rules still win.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 01:01:01 +01:00
librelad
d7d5260605 style(backup): use nebula translucent buttons; left-align location actions
Two follow-ups to the button restyle:

- On the nebula theme, primary/danger CTAs are translucent (rgba accent/danger
  fill + white text + border), not the solid generic .btn-primary. The earlier
  change only added the backup classes to the generic groups, so on nebula the
  Add location / Save changes / Delete buttons fell back to a solid fill with
  dark text. Add .backup-primary-btn and .backup-danger-btn to the
  [data-theme="nebula"] groups too, so they match the config-page buttons.

- The per-location action row used justify-content: space-between, throwing the
  two buttons to opposite edges. Switch to flex-start with a gap (like
  .config-actions) and put Save changes (primary) before Delete location.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 00:47:43 +01:00
librelad
b1983dec56 feat(webui): server-side dismissible UI notices (Dismissible helper)
Add a reusable Dismissible helper that persists 'hide this permanently' state server-side in data/ui-state.json via the existing authenticated /read-file + /write-file endpoints. It's a direct file write — no task is created (nothing in the task manager) and no system scan runs — so it sidesteps the heavyweight config_update path entirely and works across browsers/devices. The backup config-backup warning now dismisses through Dismissible instead of localStorage; any future notice can opt in with Dismissible.isDismissed(id)/dismiss(id).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 00:25:15 +01:00
librelad
4568ec51ef feat(backup): Export dropdown in Configuration header; warning is dismiss-only
Drop the Export button from the config-backup warning banner — it's now just the alert + dismiss (x). On the Configuration tab the top-right primary action becomes an 'Export' dropdown (first item: Repository Passwords, reusing the existing export-passwords action) so more export types can be added later. Other tabs keep Backup all apps / Add location. Menu opens from the trigger and closes on outside click, item click, or tab switch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 00:14:06 +01:00
librelad
14ba3b03c7 feat(backup): make config-backup warning stand out and dismissible
Add a large amber alert-triangle icon to the 'keep your config backed up offline' banner and a close (x) button in its top-right. Dismissal is stored in localStorage (libreportal:backup-config-warning-dismissed) — a per-browser UI nudge, not server config — and hides both the banner and its divider until cleared.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 00:07:38 +01:00
librelad
3bac76b3fb fix(webui): tag only the default Backup style preset and float it to top
The retention "Backup style" dropdown hardcoded "(default)" into two preset
labels, so both the global and per-location selectors showed two "(default)"
tags, and the global selector listed "Inherit global retention" — which has
nothing to inherit at the global level.

Apply "(default)" dynamically to the scope's actual default (self-hosting
globally, inherit-global per-location) via a shared retentionPresetOptions
helper that also floats that option to the top. All presets stay in the list;
inherit-global remains omitted from the global scope.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-22 14:46:45 +01:00
librelad
8406355c2d feat(backup): move 'Backup style' preset hint into a tooltip
Both Backup style controls (global Configuration retention and per-location) now surface the selected preset's description via a tooltip on the label, updated when the preset changes, instead of an always-visible hint line. Also makes the per-location tooltip accurate for non-default presets (it was previously fixed to the inherit text).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-22 14:19:53 +01:00
librelad
3b13a67ca7 feat(backup): tidy location editor — section dividers, style tooltip, row enable toggle
Make the expanded location editor read like /config: Connection and Retention now use the section header + .domains-divider layout, and Connection gets a description. Move the retention 'Backup style' guidance into a tooltip and drop the always-visible hint line below it. Move the Enabled toggle out of the Connection fields into the collapsed location row header so a location can be enabled/disabled without expanding it; setLocationEnabled persists the change via the same config_update routing as saveSection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-22 14:09:32 +01:00
librelad
a7aa050528 fix(backup): one config-fields grid for location fields, divider under config-backup banner
Render a backup location's connection fields in a single .config-fields grid like /config's renderer, instead of chunking every 3 fields. The chunking left ragged blocks whose columns stopped lining up once any field was hidden (PATH_MODE/SSH/etc.) — the grid handles row layout and drops hidden fields cleanly. Also add a config-divider below the "Keep your config backed up offline" warning banner.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-22 13:46:03 +01:00
librelad
875a60f90f LibrePortal v0.1.0 — initial release
A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys,
Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun
VPN routing, and a web dashboard to manage it all.

Free & open forever to self-host; optional paid hosted services fund it.
See PROMISE.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-21 20:37:54 +01:00