272 Commits

Author SHA1 Message Date
librelad
fa47e16cab feat(updater): automatic background scan for versions, CVEs & improvements
Replace the click-to-scan-only flow with a self-throttled auto-scan that
rides the existing task-processor idle poll (the same shape as the
network-drift check — no new daemon, unit, or endpoint):

- 'libreportal updater check auto' gates on the age of the generated
  updates.json vs CFG_UPDATER_SCAN_INTERVAL (minutes, default 30,
  0 disables); a fresh file makes the 60s tick a single stat() + return.
  Manual checks and post-update rescans reset the clock for free, and a
  missing file means the first scan runs ~a minute after install.
- Eligible signed hotfixes keep flowing through artifactApplyAuto, which
  only enqueues ordinary tasks — mutations stay on the task path.
- Open updater surfaces (standalone /updater and the fleet Overview's
  headless UpdaterPage) follow along with a 60s static-JSON re-read that
  repaints only when a generated_at stamp changed; timer released via
  dispose() on unmount, ticks skipped while hidden.
- Empty states now say the first scan happens automatically; Check now
  stays as the immediate manual override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 22:07:42 +01:00
librelad
c05e4af6f0 refactor(webui): move update status from dashboard banner to a topbar pill
The dashboard carried a persistent, full-width "LibrePortal vX.Y.Z" /
up-to-date strip — passive info occupying prime real estate above the
user's actual content, and out of place next to genuinely actionable
cards. Version/update state is chrome, so it now lives in one persistent
pill in the global topbar (every page), with detail + actions behind the
existing modal.

The pill is calm by default and escalates only when warranted:
  * up to date     -> subtle "✓ Up to date"
  * local/dev      -> neutral version chip ("v0.2.0")
  * checking        -> spinning "Checking…"
  * update waiting  -> accented, pulsing-dot "⟳ Update"
Clicking opens the details modal (unchanged) for the full readout and the
Update now / Check for updates actions.

The dashboard update banner is removed. The network-notifier's banner
stays — it's attention-only (shown solely on a real, actionable network
conflict), which is exactly when a dashboard banner earns its place; its
topbar badge now anchors after the pill, and the dashboard data-loader
re-asserts that banner on mount.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 19:47:41 +01:00
librelad
3afe40bbbc refactor(overview): de-chrome the embedded Backups + Peers sub-tabs
The Backups tab's embedded BackupPage repeated the active section as a
big page header (icon + 'Dashboard' + subtitle) right under the nested
strip that already names it. Embedded-scoped CSS now hides the title
block and flips the header below the body (flex order), so its actions
(Refresh + per-section primary) become a bottom-left footer row — the
same place app-detail tabs keep theirs. The export dropdown flips to
open upward from the footer. The standalone /backup page is untouched.

The Migrate ▸ Peers sub-tab drops its page header (breadcrumb + title
+ blurb) the same way: the peer list/empty state now sit in the shared
recessed .ov-tab-body container with the four actions in a bottom-left
.peers-actions footer.

Signed-off-by: librelad <librelad@digitalangels.vip>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 19:41:35 +01:00
librelad
a06b6cd1d8 feat(overview): match fleet tab content to the app-detail tab layout
Every fleet Overview tab now follows the per-app detail tab idiom the
rest of the app uses: title + description on the left, action buttons
on the right, a divider underneath, and the body inside the recessed
dark container (.tasks-container recipe).

- renderHeader() gains an action slot; Check/Check now/Update all move
  out of in-body toolbars into the header (Updates keeps its filter
  chips in the body; the Apps-tracked stat card drops its duplicate
  Check button; UpdaterPage.renderImprovements can skip its toolbar).
- String tabs wrap their body in .ov-tab-body — margin/padding 16px,
  rgba(bg,.2) panel — mirroring backup/tasks/updater containers.
- The Backups tab's embedded nested strip (Dashboard/Backups/Locations/
  Configuration) now sits on the same surface as every other tab strip:
  added to the nebula sidebar-bg anchor rule (it was stuck on the
  lighter --hover-bg) and its buttons use .main-tab-button type.

Signed-off-by: librelad <librelad@digitalangels.vip>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:55:10 +01:00
librelad
9582671072 feat(tasks): path-based single-task permalink (/tasks/<cat>/<id>)
The global tasks page still deep-linked a single task with a ?task=<id>
query while the rest of the SPA moved to path-based permalinks
(/app/<name>, /admin/<…>). Bring it in line: the task is now a path
segment, /tasks/<category>/<id>.

Task ids are guaranteed `task_<digits>_<base36>` (isValidTaskId), so the
redundant `task_` prefix is dropped in the URL and restored on read via a
new window.taskPath / window.taskPartsFromPath helper pair (mirrors
appPath/appPartsFromPath). The parser still accepts the legacy ?task=
query and the full-prefixed id, so old links, bookmarks and notifications
keep resolving.

Updated every builder (tasks-manager updateURL + notification url,
task-id link, task-actions, admin config-form, setup-wizard handoff with
its &from=setup flag) and the notification navigation handler / button
text to recognise the path form.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 18:31:18 +01:00
librelad
9dace1ed95 feat(tasks): auto-select the running task on the tasks page
Landing on /tasks (directly, via a deep link, or from the setup-wizard
handoff) now opens the row the visit is actually about:

- init() re-reads the URL on every SPA (re)mount, so ?task= deep links
  work after the first visit instead of using constructor-stale state.
- applyInitialSelection() opens the deep-linked task, or — for setup
  handoffs whose first task the queue has already moved past, and for
  plain visits with no deep link — the currently running task (else the
  next queued one).
- The selection then follows the queue: when a new task starts running
  the open panel moves with it, until the user manually toggles a row
  or switches category (their choice then wins for the visit).
- selectTask() is the shared programmatic open: exclusive expand, live
  log stream for active tasks, smooth scroll into view.

Signed-off-by: librelad <librelad@digitalangels.vip>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:27:06 +01:00
librelad
2188a99787 fix(apps): make the instance family bar a full-width row
The app-detail header is a flex row, so the switcher rendered inline to the
right of the service buttons. Wrap the detail header and make the bar span
100% so it sits under the title/service buttons, above the tab strip, as
intended.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-05 00:21:02 +01:00
librelad
ab1d335d35 refactor(apps): manage instances on the type's page, not the grid
Replaces the per-instance grid cards + per-card "New instance" action with a
single card per app type and an in-page family switcher — the UX you asked
for (swap between instances via LibrePortal navigation; manage on the page).

- Grid: hide any app declaring INSTANCE_OF (loadApps filter), so there's one
  card per type. A subtle "N instances" chip replaces the old card button.
- App detail: a "family switcher" bar under the title for multi-instance
  types and their instances — a pill per member (base + each instance) that
  path-navigates to that slug's detail (current tab kept), plus "+ Add"
  (existing create modal). When viewing an instance, a "Remove instance"
  action sits in the bar.
- Remove flow: instance_remove task verb (-> existing `libreportal instance
  remove`) with an on-brand confirm modal; lands back on the base type page.

Frontend-only; the instance create/remove backend and CLI are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-05 00:17:30 +01:00
librelad
376610cd11 feat(apps): scoped multi-instance support (run two of an app)
Lets a *multi-instance-capable* app run as several fully isolated instances
on one box (e.g. two Bookstack/WordPress sites, or a "family" + "work"
Nextcloud) — distinct data, DB, subdomain, backups and update cadence.

Design: an instance is just another app. It gets its own slug (<type>_<id>),
its own CFG_<SLUG>_* namespace, deployed dir, DB row, IP/port allocation and
host, so the entire existing pipeline (scan, install, services, routing,
updater, backups) treats it like any app with zero changes. All
instance-specific rewriting is confined to a clone of the type's template;
the shipped template and the core engine are untouched.

Gating: opt-in per app via CFG_<TYPE>_MULTI_INSTANCE=true. Only Bookstack
carries it for now (the validated reference). The other 31 apps are
unaffected — the feature is invisible unless the flag is present.

- scripts/instance/instance_create.sh — clone + re-namespace config, rewrite
  compose identity (container_name / Traefik routers / backup labels) and
  per-app tools, set a hostname-safe subdomain (PORT field 10), then hand off
  to dockerInstallApp. Plus instanceList / instanceRemove.
- libreportal instance create|remove|list — new CLI category; mutations route
  through the task system (no new mutating API endpoint).
- WebUI: "instance of <type>" badge + a "New instance" card action on capable
  apps, and a create modal (name + domain# + subdomain, live host preview)
  that dispatches the standard task. Capability/instance-of read straight off
  the already-exposed app config.

Known follow-ups (documented): flip the flag on more apps after a compose
identity check (Nextcloud next); per-app tools are best-effort isolated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-04 23:34:52 +01:00
librelad
8006ddba75 fix(webui): give the app-detail Updates tab the standard tab chrome
The per-app Updates tab rendered a bare version/badge bar + detail with no
title, no dividers and no recessed container — unlike the Config / Backups /
Tasks tabs.

- Add a .updater-title block (⬆️ Updates + description, Check/Update actions,
  bottom-border divider) mirroring .backup-title.
- Wrap the body in a .updater-detail-container recessed dark panel (same recipe
  as .backup-snapshots-container / .tasks-container).
- Separate the Version/Security/Recovery/History sections with divider lines
  (scoped to the container; fleet row-details keep their gap-only spacing).
- renderAppDetail() gains an opt-in Version section so the version/badge reads
  as a section in the panel; fleet rows omit it (the row head shows it already).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 00:56:01 +01:00
librelad
25dc51d63e fix(webui): make Overview sub-tab areas match the app-detail layout
Migrate and Backups detached their sub-tab strip from the content (16px gap)
and Backups had no top-level header and a transparent (card-less) body — so
both read as different from the per-app Config sub-tabs.

- Migrate: nest .tabs-content inside the same .tabs-wrapper as .tabs-list (the
  canonical app-detail structure) and drop the gap, so the strip joins the
  content card with no space and clean corners.
- Backups: inject the shared "Backups" .config-title header above the embedded
  BackupPage; drop the sub-tab strip's bottom gap; give the content (.main) the
  connected .tabs-content surface (card bg + rounded bottom) and round the
  strip's outer top corners — so it reads as one tabs-within-tabs unit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 00:23:10 +01:00
librelad
4d54d6a9b0 feat(webui): unify Overview tab headers as in-content, app-detail style
Drop the floating fleet header; every Overview tab now renders its heading
inside the pane, under the tab strip, in the per-app detail .config-title
format (emoji + title + description) — matching the Migrate tab.

Introduces a small modular system so the area can't drift:
- OV_TAB_META is the single source of truth for each tab's icon/title/blurb.
- renderHeader(id) is the only thing that turns it into markup; renderTab()
  prepends it for the string tabs and mountMigrate() injects it once for the
  static Migrate pane. Body renderers now only ever produce the body.

Retires the now-dead floating-header plumbing: updateHeader(), the
ov-backups-active/ov-migrate-active hide toggles + CSS, and the .overview-header
rules. The tabbed interface owns the top padding the header used to provide.

Backups is the documented exception — it embeds the full BackupPage, which
supplies its own header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:29:16 +01:00
librelad
f6f29bf68b feat(webui): match Migrate tab to app-detail tab design
Give the fleet Overview › Migrate tab the same in-content header + sub-tab
styling the per-app detail tabs use:

- Add a .config-title header (icon + title + description) inside the Migrate
  pane, above the Restore/Peers sub-tabs, and hide the generic floating fleet
  header for Migrate (mirrors how Backups already supplies its own heading).
- Make the .tab-button icon↔label gap explicit (flex + 6px gap) so sub-tabs
  render identically whether or not the markup has whitespace between the
  emoji and name spans; align the Backups sub-tab strip gap to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:04:44 +01:00
librelad
9986a8a814 fix(webui): stop rapid clicks selecting text on setup level cards
The Beginner/Advanced experience cards are tap targets, but had no
user-select guard. Rapid clicking — notably the Advanced card's 10-tap
dev-mode unlock — selected the card title/description text as a side
effect. Add user-select: none to .setup-level-card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-02 16:21:39 +01:00
librelad
f1e1330cd8 feat(webui): network-drift notifier (topbar badge + dashboard banner)
Surfaces network_status.json in the WebUI, attention-only: a rose badge
+ dashboard banner appear ONLY when conflicts_found, with a details panel
listing the stranded apps and a 'Heal now' button that runs the heal
through the task pipeline (libreportal system network heal). Re-reads on
task completion via taskRefresh.

Cloned from update-notifier; the badge anchors just after the update
badge so the two coexist in a stable order. New --page-network hue.
Wired into system-loader scripts[], topbar onTopbarReady, and index.html.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:07:54 +01:00
librelad
164f782a95 fix(webui): address Migrate-refactor review findings
- Restore empty-state 'Open Locations' now deep-links to the backup center's
  Locations sub-tab: the embedded center honors /overview/backups/<sub> and
  switchTab()s to it after mount (was landing on Dashboard).
- PeersPage.notify + MigratePage.notify use the real window.notificationSystem
  (were calling a never-defined window.showNotification → console-only).
- Remove the now-dead Admin config-manager peers branch (Peers left Admin).
- Trim the dead migrate.json/peers fetch + hostnameToPeerName from BackupPage
  (no consumer after the migrate removal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:12:11 +01:00
librelad
69bb5532b7 refactor(webui): move Peers out of Admin; harmonize Backups sub-tab strip
- Remove the Admin sidebar Peers entry; /peers and /admin/tools/peers now
  redirect to /overview/migrate/peers (its new home next to cross-host Restore).
- Re-skin the embedded Backups center's sub-tab strip from pills to the per-app
  Config .tabs-list/.tab-button segmented look (full-width bar, accent underline)
  so every nested sub-tab row is consistent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:46:30 +01:00
librelad
25e25230fd fix(webui): drop deleted backup-migrate.js from the embedded center asset list
The embedded backup center's lazy bundle still listed backup-migrate.js, which
was just removed — its 404 failed the whole load chain. Drop it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:39:36 +01:00
librelad
c449641b9c refactor(webui): remove Migrate from the backup center (moved to Overview)
Migrate now lives at Overview › Migrate › Restore (standalone MigratePage). Strip
it out of BackupPage: drop the migrate sidebar item/panel/modal from the
fragment, the 'migrate' tab from the allowed set / titleFor / subtitleFor /
iconFor, the renderMigrate() call, and the migrate-host/app/confirm click
handlers; delete the now-orphaned backup-migrate.js. The backup center is now
Dashboard/Backups/Locations/Configuration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:37:46 +01:00
librelad
ebdae15838 feat(webui): deep-link the Migrate sub-tabs (/overview/migrate/{restore,peers})
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:28:58 +01:00
librelad
4a964c42a2 feat(webui): add Migrate fleet tab (Restore + Peers sub-tabs)
New 5th Overview tab 'Migrate' with a nested segmented sub-tab row reusing the
per-app Config-tab .tabs-list/.tab-button design:
- Restore: a standalone MigratePage (cross-host migrate moved out of BackupPage
  into its own controller + fragment + modal; own data fetch + task dispatch).
- Peers: reuses the existing PeersPage (container-parameterized) + its template.
Both lazy-loaded on first open and disposed on apps-feature unmount. Additive —
migrate is still in the backup center and Peers still in Admin until the next
commits remove the duplicates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:24:54 +01:00
librelad
efdbed8e0c fix(webui): embedded backup-center review fixes
- BackupPage task-refresh run-guard is instance-relative now (window.backupPage
  OR window.overviewBackupPage), so the embedded center's own auto-refresh works
  instead of relying on OverviewManager's overlapping coverage.
- _ensureBackupAssets no longer memoizes a rejected promise — a transient
  script-load failure no longer bricks the backup center until reload; the next
  open retries.
- spaClean.loadScript removes the failed <script> element on error so the
  getElementById dedupe can't make a retry resolve without re-fetching.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 01:55:09 +01:00
librelad
f1f0cf7516 fix(webui): scope embedded backup-center mount check to the pane
The 'already mounted?' guard checked #backup-section, but the per-app Backups
tab also defines that id — detect the prior mount via the pane's own
.backup-layout instead so the check is correct, not coincidental.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 01:40:29 +01:00
librelad
c508a20605 feat(webui): embed the full backup center in the Overview Backups tab
Instead of a glance + 'Open backup center' button, the Backups tab now mounts
the real BackupPage (dashboard/snapshots/locations/migrate/configuration) inline,
with its sidebar restyled as a nested tab strip and its own header taking over.

- BackupPage gains an embedded mode (opts.embedded): no /backup URL coupling, so
  sub-tabs switch in-page under /overview/backups. Backward compatible.
- OverviewManager lazy-loads the backup bundle + fragment on first open, news a
  BackupPage({embedded:true}), and disposes it on apps-feature unmount. Colliding
  ids (#sidebar/#mobile-overlay) are stripped on inject.
- Revert the Admin backup-config surface — the embedded center (incl.
  Configuration) is now the single home for backup settings.
- The updater needs no equivalent: its sections were already unpacked into the
  Overview/Updates/Improvements tabs + the per-row expander.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 01:36:25 +01:00
librelad
d5e2375f38 fix(webui): address review findings on the fleet Overview build
- HIGH: renderAppDetail gated the Roll back button on last_snapshot* fields no
  generator emits, so it never rendered. Derive recoverability from update
  history (updater_apply always snapshots first) so the affordance is reachable.
- MED: per-app Updates tab now repaints on update/rollback/check/hotfix task
  completion (mirrors the backups card) instead of going stale until re-click.
- MED: in-page tab switches now sync spaClean.currentRoute, so the sidebar
  Overview entry no longer no-ops after switching tabs.
- LOW: keyboard activation (Enter/Space) for the role=button expander heads,
  backup tiles, and sidebar Overview entry.
- LOW: preserve ALL expanded Updates rows across a background repaint, not just
  the single ?app= deep-link (split toggle into _openDetail/_closeDetail).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 00:37:59 +01:00
librelad
dfd4ffa268 feat(webui): drop Updates & Backups top-nav items; fold into App Center
The fleet Overview area (Overview · Updates · Improvements · Backups) and the
backup center now live under App Center, so the standalone top-nav items are
redundant. Top nav is now Dashboard · App Center · Admin · Tasks.

- Remove the Backups and Updates anchors from topbar.html.
- Remove the nav{} blocks from updater/backup feature.json + manifest (so they
  don't resurface when the nav kernel lands).
- Highlight App Center for /overview and /backup; drop the dead /updater branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 00:10:30 +01:00
librelad
c7ae1414b9 feat(webui): redirect /updater into Overview; surface backup config in Admin
- _legacyRedirect: /updater[/tab] -> /overview[/tab] (security/recovery/history
  fold into the Updates expander -> /overview/updates). /backup is intentionally
  NOT redirected — it stays the operational backup center (locations/migrate/
  snapshots), reached from Overview › Backups.
- Re-point the per-app hotfix chip to /overview/improvements.
- Unhide the existing backup config category in the Admin sidebar so
  engine/schedule/retention config lives under Admin (same generated category
  the backup center binds, so edits stay in sync).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 00:01:59 +01:00
librelad
1460acb941 feat(webui): add per-app Updates tab (version/CVEs/recovery/history)
New 'Updates' tab in the app detail page, beside Backups. Reuses the headless
UpdaterPage + renderAppDetail() scoped to the single app, so the per-app and
fleet views share one data/render path. UpdaterPage is added to the apps script
bundle so it's available on app pages; the tab is disabled while a task runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 23:54:21 +01:00
librelad
dbc5e64505 feat(webui): deep-link auto-expand for Overview Updates rows
Open the per-app row named by ?app=<name> on load/repaint and write it back on
toggle, so an expanded Updates row is a shareable URL — mirrors the Tasks page's
?task=<id> pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 23:46:34 +01:00
librelad
8acf2d02c3 feat(webui): add fleet Overview area (Overview/Updates/Improvements/Backups tabs)
Introduce a per-fleet Overview area inside the apps shell, reachable from a new
'Overview' entry pinned above the apps sidebar search. Selecting it renders a
top-tabbed view in the main pane — Overview · Updates · Improvements · Backups —
mirroring the per-app tabbed layout, with the apps sidebar persistent.

- TabController: generic root-scoped show/hide tab host (core/ui-state).
- OverviewManager: drives the 4 tabs. Reuses a headless UpdaterPage for all
  update/CVE/improvement data + rendering (its renderX() are pure HTML
  producers) and reads backup/dashboard.json directly for backup health.
- Overview tab: combined update + backup health cards.
- Updates tab: per-app expander table (CVEs/recovery/history inline via the new
  UpdaterPage.renderAppDetail) + All/Updates/Security filter chips.
- Improvements tab: reuses the updater's signed-hotfix renderer.
- Backups tab: fleet backup-health tiles; actions deep-link per app.
- Additive only: /overview* routes on the apps feature; old /updater and
  /backup pages untouched. Cleanup (redirects, nav, Admin config move) is next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 23:37:35 +01:00
librelad
79d2a4750d feat(webui): Phase 4 — Improvements (hotfix) stream + per-app chip
Surfaces the hotfix channel in the WebUI. Primary home is the Updates &
Improvements page (the updater component) — its own "Improvements" tab — with a
secondary chip on the App detail page (fork 3 locality = both).

Updater component (components/updater):
- New "Improvements" sidebar tab + panel; renderImprovements() reads the host-
  generated artifacts_available.json (severity badge, scope chip, applied/auto/
  not-applicable badges, plain-English why). Apply/Revert buttons dispatch
  artifact_apply / artifact_revert through the TASK system (services.tasks.route)
  — no mutating API. Apply is disabled when the index is UNSIGNED.
- Overview gains an "Improvements" stat card; task-refresh now also repaints on
  artifact_* task completion; URL tab routing + dispose teardown extended.

Task plumbing (core/tasks): artifactApply/artifactRevert action methods (id is
charset-guarded before it enters the command string) + artifact_apply/
artifact_revert routeAction cases. Task list/format gain icons + friendly labels.

Apps component: an amber " N improvements" chip on an installed app's detail
header (populated async from artifacts_available.json filtered by app, applicable
& not-applied), linking to /updater/improvements. Best-effort, never throws.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 21:07:01 +01:00
librelad
306e6223c0 fix(webui): release leaked listeners/intervals/streams on unmount (all modules)
The teardown audit found the backup-stacking leak class across 4 more feature
modules (12 confirmed leaks); unmount() left document/window listeners, intervals,
and SSE subscriptions firing on stale controllers after navigation:

- admin: overview/ssh/peers/system each leaked a document click listener ->
  AbortController + dispose() per page; admin unmount() aborts each.
- dashboard: the 1 Hz update-countdown interval + the LiveSystem view sub ->
  stopUpdateCountdown()/detachDashboardLive(), registered via ctx.sub().
- tasks: constructor-started global live-log poller (discarded handle) -> stored
  + idempotent + cleared on unmount + re-armed on mount; per-task monitorTask
  window listeners + interval -> tracked in a map, released on unmount.
- apps: app-tabbed reconcile setTimeout loop + watchdog window/document listeners
  + popstate -> per-instance AbortController + dispose() that clears the timer,
  resets the guards, and unloads the active tab's Services intervals + log SSE.

All mirror the kernel's MountContext teardown discipline. 12 files, all pass
node --check. Backup (fixed earlier) re-confirmed clean by the audit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 15:27:29 +01:00
librelad
7709b0d97b fix(backup): dispose document listeners on unmount (sidebar stacking bug)
The original report: clicking a backup sidebar tab loaded content on top of
the old content. Root cause (flagged in the unmount comment as deferred):
BackupPage.bindEvents() attaches document-level click/input/change listeners
guarded only by the instance-level this.eventBound, and unmount() nulled
window.backupPage WITHOUT removing them. Each revisit added another full set of
listeners bound to a stale BackupPage, all firing on every click and mutating
the live DOM (double tab-switches, double modal opens, stale-instance renders).

Fix (mirrors the kernel's MountContext pattern): give BackupPage an
AbortController, bind the three document listeners to its signal, add dispose()
that aborts them (+ drops the task-refresh reg + clears the timer), and call it
from the feature module's unmount(). Revisits now start clean — one live
instance, one set of listeners.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 14:48:54 +01:00
librelad
4e18a6ff42 fix(webui): render App-Updater tasks as standard tasks in the panel
updater_check/apply/apply_all/rollback tasks fell through every per-type
branch of the Tasks panel, so they showed the generic custom gear icon, a
raw/truncated command title, and (for the app:'updater' sentinel) a broken
hidden app icon. Wired them in like every other task type:

- tasks-format.js formatCommandForUser PATTERNS: added the 'libreportal updater'
  command rows (Apps - Check for Updates / Update All / <App> - Update /
  <App> - Roll Back) — only the *self*-update 'libreportal update' was mapped.
- tasks-format.js formatActionTitle: added the updater_* short labels.
- tasks-list-render.js getTaskTypeIcon: updater_check 🔍 / apply ⬆️ /
  apply_all ⬆️ / rollback ↩️ (reusing existing verify/update/restore classes).
- tasks-list-render.js renderTaskIcons: treat app:'updater' as a sentinel like
  app:'system' so updater_check/apply_all fall back to the LibrePortal logo
  instead of a 404'd /core/icons/apps/updater.svg (apply/rollback keep their
  real app icon).

node --check clean.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 02:53:57 +01:00
librelad
bb0d750b55 chore(webui): remove two orphan icons
Verified-dead assets from the feng-shui audit, zero consumers:
- core/icons/categories/utils.svg — no 'utils' app category exists (the only
  'utils' refs are unrelated system health-check names); category icons are
  requested as /core/icons/categories/<id>.svg and no id is 'utils'.
- core/icons/apps/portainer.svg — Portainer was retired to
  scripts/unused/OLD_CONTAINERS/; no live containers/portainer/, and apps.json
  is generated only from live containers, so the icon is never requested.

Both git-recoverable if a portainer app / utils category is ever (re)added.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 01:34:51 +01:00
librelad
c22b0ac60d chore(webui): strip ~665 commented-out console.* debug lines
The shipped frontend carried ~600 muted '// console.…' debug statements (and
their multi-line commented continuation lines) left over from development —
clutter across 30 files. Removed them with a guarded pass that ONLY ever deletes
lines starting with // (so it can never alter behaviour), consuming each
commented console opener plus its continuation comment lines until the
string-stripped parens balance.

665 lines removed, 30 files; 0 insertions. Verified every deleted line is a //
comment (no code touched), real prose comments preserved, full node --check
sweep clean.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 01:25:41 +01:00
librelad
c920ca2dc9 refactor(webui): align page-controller names with the -page convention
From the feng-shui audit naming findings:
- admin/overview/js/admin-overview.js -> overview-page.js (class AdminOverview ->
  OverviewPage, window globals + the 'admin-overview' task-refresh id ->
  overview-page, lazy-load path + typeof/new in config-manager.js).
- admin/system/js/admin-system.js -> system-page.js (class AdminSystem ->
  SystemPage; now sits beside its -page sub-views system-metric-page.js /
  system-storage-page.js).
- tasks/js/tasks-logs-modal.js -> tasks-log-modal.js (singular 'log' to match its
  sibling tasks-log-stream.js; single path ref in system-loader.js).

These were the only page controllers breaking the dominant <thing>-page.js /
<Thing>Page convention (ssh-page/peers-page/backup-page/updater-page/
system-metric-page/system-storage-page). Pure renames; node --check clean.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 01:21:07 +01:00
librelad
afe0ef1c7e chore: drop duplicate doc files + fix wrong/stale comments
- docs: remove the docs/README.md index and docs/CONTRIBUTING.md pointer
  (duplicate filenames); the canonical contributing guide stays at
  docs/contributing/contributing.md. Clean tree, no name collisions.
- scripts/system/*: 6 helper headers + host_access.sh said the helpers
  install to /usr/local/sbin, but init.sh installs all of them to
  /usr/local/lib/libreportal/ (verified via initRootHelpers + the sudoers
  Cmnd_Alias). Corrected. The only remaining /usr/local/sbin is the legit
  PATH export in the task processor.
- frontend kernel: drop migration-era comments that are now false post-
  modularization (feature-registry 'passive/phase 0/unused', lifecycle
  'ctx.services lands with Phase 2', manifest 'scan generator lands') —
  describe current behaviour instead.

Comment-only edits to scripts/system/* — no footprint_version bump (no
behavioural change; bumping would force needless reinstalls).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 01:05:16 +01:00
librelad
30612a0d87 docs: organize docs/ into purpose folders with consistent naming
Sort docs/ into guide/ contributing/ architecture/ roadmap/ and rename
to consistent kebab-case (USER->guide/install-and-use, FOOTPRINT->
architecture/system-footprint, frontend-modularization->architecture/
webui-architecture, etc.). Add a docs/README.md index and a docs/
CONTRIBUTING.md pointer so the forge still surfaces the contributing
guide. Fix every reference (README, init.sh comments, frontend code
comments, and the USER<->DEVELOPMENT cross-links). History preserved
via git mv. Root stays README.md + CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 00:48:38 +01:00
librelad
164606dc7c docs(webui): refresh stale features/ path comments after the components/ rename
Comment-only tidy from the feng-shui audit — no code behavior changes. The
features/ directory was renamed to components/ during modularization, but
several header banners and inline comments still named the old path:

- 6 component module headers (admin/tasks/backup/dashboard/updater/index.js +
  updater/js/updater-page.js) now name their real components/<id>/… path
- core/kernel/js/spa.js + core/tasks/js/task-router.js comments
- backend/routes/features.js doc-banner (drop a components/<id>/ folder …)
- core/update-notifier/css/update-notifier.css header (js/update-notifier.js)

Guarded the rewrite so the LIVE /api/features/list endpoint name (feature-
registry.js sources + backend route) is untouched — only stale 'features/<path>'
directory references were updated.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 00:41:16 +01:00
librelad
123f04b03e Merge claude/2 2026-05-31 00:39:40 +01:00
librelad
19909b91e0 docs: rewrite frontend-modularization as a lean as-built reference
The modularization shipped (2026-05-30), so the design doc was stale and
internally contradictory: it described a features/ tree (real tree is
components/), a shell-generator/Node route that were never built, and a
'partially implemented' status. Replace the 59KB design exploration with
a short, accurate description of the component-module system as it exists
(components/<id> pages, core/ subsystems, the kernel: feature-registry/
services/lifecycle/spa, static manifest discovery, mount/unmount contract,
eager global CSS). Fix one stale features/ path in a spa.js comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 00:39:12 +01:00
librelad
8d86855098 fix(webui): category-icon fallback + flatten forms/ + manifest order
From the feng-shui audit (all adversarially verified):

- BUG (high): apps-grid.js category tiles used onerror fallback
  /core/icons/categories/default.svg, which doesn't exist (the dir has
  misc.svg as its generic icon, which data-loader.js already uses). Any
  category missing its named SVG showed a broken-image glyph. Repointed to
  /core/icons/categories/misc.svg.
- TIDY: core/forms was the lone depth-3 nesting — JS at forms/controls/js/
  while its CSS sat at forms/css/ and every other core subsystem uses
  <name>/js/. 'controls/' grouped nothing (just the 2 custom-* widgets), so
  flattened to core/forms/js/ (+ updated index.html). forms is now symmetric.
- CONSISTENCY: components/manifest.dev.json entries carried nav.order but not
  the top-level 'order' that each feature.json has; added it so the API-down
  fallback matches the live /api/features/list scan.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 00:39:08 +01:00
librelad
5cf5b88b16 fix(webui): flatten redundant components/admin/config/icons/config/ + repair engine logos
The config-category icons sat at admin/config/icons/CONFIG/ — the inner config/
duplicates the subsystem name; they belong in the icons root. Moved all 6
(backup, features, general, network, security, webui) up to
components/admin/config/icons/ and updated the two consumers (config-manager.js
header icon, config-sidebar.js category icons).

Also fixed the backup-engine logos: scripts/backup/engines/{restic,kopia,borg}
.json pointed 'logo' at /icons/config/backup.svg — a path that 404'd on two
counts (missing the components/admin/config prefix AND the now-removed config/
nesting), so the engine-details modal logo silently hid. Repointed to the real
served path /components/admin/config/icons/backup.svg.

(Left the meaningful icon groupings alone — admin/system/icons/{cpu,os} and
apps/core/icons/vpn are vendor/OS/provider logo sets, not redundant nesting.
The backup engines borrowing an admin-config icon is a minor smell; a dedicated
backup-engine icon could replace it later if wanted.)

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 00:07:49 +01:00
librelad
fe5cc18827 refactor(webui): give every core/ subsystem the js/css/html convention
Brings core/ in line with components/ — each subsystem now sorts its files into
js/ css/ html/ subfolders (and the nested auth/ + controls/ groups keep theirs):

  core/topbar/{js/{topbar,mobile-menu}.js, css/{topbar,sidebar}.css, html/topbar.html}
  core/theme/{js/theme-registry.js, css/{tokens,themes,base,aurora-background}.css}
  core/forms/{css/{forms,config}.css, controls/js/{custom-number,custom-select}.js}
  core/boot/{js/{system-loader,system-orchestrator}.js, auth/{js/auth-manager.js,css/login.css}}
  core/{config,tasks,kernel}/js/…  core/overlays/{js,css}/…  core/setup/{js,css}/…
  core/{app-meta,backup-card,data-loader,dom,live,notifications,ui-mode,ui-state}/js/…
  core/{loading,update-notifier}/{js,css}/…

50 files relocated (pure git mv). All path literals rewritten from a generated
old→new map across index.html, system-loader.js bundles, topbar.js's internal
fetch (/core/topbar/html/topbar.html), and the three backup-app-card loaders. No
OLD path contained a js/css/html segment, so no double-prefixing was possible.
core/icons/ left as-is (shared asset tree). All 50 /core asset refs verified to
resolve; full node --check sweep clean.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 00:00:52 +01:00
librelad
5351da5b4c refactor(webui): regroup core/ui + core/boot into named subsystems
Final structural phase — the catch-all core/ui/ is gone and core/boot/ keeps
only the genuine bootstrap pipeline:

  core/ui/eo-modal.js, confirmation-dialog.js   -> core/overlays/
  core/ui/notifications.js                       -> core/notifications/
  core/ui/topbar.{js,html}, mobile-menu.js       -> core/topbar/
  core/ui/update-notifier.js                     -> core/update-notifier/
  core/ui/backup-app-card.js                     -> core/backup-card/  (shared widget)
  core/boot/controls/                            -> core/forms/controls/
  core/boot/setup/                               -> core/setup/
  core/boot/loading-ui.js                        -> core/loading/
  core/boot/auth-manager.js                      -> core/boot/auth/

Each subsystem now owns its JS + CSS together (topbar.js beside topbar.css,
auth-manager beside login.css, etc.). Path-only moves; rewrote literals in
index.html, system-loader.js (confirmation/notifications/topbar/mobile-menu/
update-notifier/apps bundles), topbar.js's internal fetch('/core/topbar/
topbar.html'), and all THREE backup-app-card loaders (system-loader, backup/
index.js, spa.js). core/boot now = system-loader, system-orchestrator, auth/.
All 24 referenced paths resolve; full node --check sweep clean.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 21:34:18 +01:00
librelad
2da0b22719 refactor(webui): dissolve core/css — co-locate each sheet with its owner
The generic core/css/{base,components,screens} buckets are gone; every shared
stylesheet now lives beside the subsystem that owns it:

  base/tokens.css, base/themes.css, components/aurora-background.css -> core/theme/
  base/style.css                                  -> core/theme/base.css (carve deferred)
  components/modal.css                            -> core/overlays/
  components/topbar.css, components/sidebar.css   -> core/topbar/
  components/forms.css, components/config.css     -> core/forms/   (config.css under forms)
  components/update-notifier.css                  -> core/update-notifier/
  screens/login.css                               -> core/boot/auth/
  screens/loading-screen.css                      -> core/loading/
  screens/setup-wizard.css                        -> core/setup/

href-only rewrites in index.html; the <link> line ORDER is unchanged, so the
cascade is preserved (no @import anywhere). All 13 /core css hrefs verified to
resolve. (The JS for overlays/topbar/forms/update-notifier/loading/setup/auth
co-locates in the next phase.)

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 21:27:51 +01:00
librelad
461dfe1bdc fix(webui): rename core/data → core/data-loader (deploy excludes 'data/')
The Phase-2 rename put DataLoader in core/data/, but update.sh's deploy rsync
uses --exclude 'data/' (to protect the runtime frontend/data/ dir the backend
serves auth-gated under /data). rsync's pattern matches a 'data' dir ANYWHERE in
the tree, so core/data/ was silently excluded from the served copy — the file
404'd and the dashboard showed 0 apps / Loading… while every sibling subsystem
deployed fine. Renamed the folder to core/data-loader/ (segment 'data-loader' ≠
'data') so it ships. No code/class change.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 21:22:01 +01:00
librelad
de25262595 refactor(webui): kill core/lib — promote to named subsystems
The generic core/lib/ wrapper (and its task/config/util sub-buckets) is gone.
Each child is now a named core subsystem describing what it IS:

  core/lib/task/            -> core/tasks/      (task kernel: bus, refresh,
                                                 manager, router, actions,
                                                 commands, parameter-preserve)
  core/lib/config/          -> core/config/     (config-shared.js→field-factory.js,
                                                 config-options.js→options.js;
                                                 options-before-factory order kept)
  core/lib/util/system-live -> core/live/live-system.js
  core/lib/util/lp-ui       -> core/ui-mode/lp-ui.js   (stays FIRST eager — no FOUC)
  core/lib/util/data-loader -> core/data/data-loader.js
  core/lib/util/dom-helpers -> core/dom/dom-helpers.js
  core/lib/util/ui-helpers  -> core/app-meta/app-helpers.js  (getAppIcon survivor)
  core/lib/util/dismissible -> core/ui-state/dismissible.js  (generic+eager, stays
                                                 core — NOT a backup widget)
  core/boot/theme-registry  -> core/theme/theme-registry.js  (theming, not bootstrap)

Path-only moves (git mv) + literal rewrites in index.html, system-loader.js
(config/task/apps bundles) and apps-manager ensureTaskScripts. Class/global
names unchanged (ConfigShared/ConfigOptions/LiveSystem/getAppIcon) so consumers
are untouched. All 16 referenced paths verified to resolve; full node --check
sweep clean.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 21:10:27 +01:00
librelad
afb44c2f78 refactor(webui): remove dead code from core + fix two latent bugs
Verified-dead removals (zero consumers, confirmed by adversarial dependency
audit):
- core/lib/util/router.js — legacy class Router superseded by kernel/spa.js;
  self-instantiated, never exposed, and added a SECOND competing popstate
  listener. Dropped the file + its eager index.html tag.
- core/lib/task/task-global-functions.js — wired window.installApp/uninstallApp/
  etc. that nothing calls (real calls go through class methods / the task
  router). Dropped the file + its task-system scripts[] entry + the
  setupTaskGlobalFunctions() block in system-loader.js.
- TopbarComponent.createNavigationHighlighting + clearAllNavigationHighlighting —
  dead statics; window.topbarNavigationHighlighting was never set.
- ui-helpers.js: getAppStatus/formatAppName/getAppShortName (dead), the stale
  setupMobileMenu/closeMobileMenu (superseded by core/ui/mobile-menu.js's
  #mobile-drawer impl), setupActiveNavigation + the safe* helpers (verbatim dups
  of dom-helpers). Only getAppIcon remains. dom-helpers loses dead
  setupActiveNavigation + waitForElement; it is now the sole safe* source.

Bug fixes surfaced during the audit:
- system-orchestrator.js called this._wireLogout() which is defined nowhere —
  threw on the 'Continue Anyway' boot path. Removed the dangling call (logout is
  wired in topbar.setupLogout()).
- active-nav highlighting never updated on SPA navigation (it depended on the
  never-set global). spa.js now calls the live window.topbar?.setActiveNav?.()
  after each route handler.

No structural moves yet; full node --check sweep clean.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 21:04:09 +01:00