46 Commits

Author SHA1 Message Date
librelad
07daa7555a fix(webui): load mobile-menu.js; prune orphaned task queue entries
Both found in a user's console log.

1. ReferenceError: setupMobileMenu is not defined (dashboard.js:98)

   core/topbar/js/mobile-menu.js defines that global, and index.html
   never loaded it. dashboard.js called it unguarded as the FIRST line
   of setupEventListeners, so dashboard init threw every page load and
   took loadInstalledApps() with it — and the burger menu was dead on
   mobile. system-loader already guarded its own call with a typeof
   check, which is why this survived unnoticed.

   Loads the script (before dashboard.js) and guards the call, so
   optional nav chrome can never take down the page below it again.

2. Endless 404s on /api/tasks/<id> for tasks that no longer exist

   queue.json is append-only from the enqueue side and nothing ever
   pruned it, so any task file removed afterwards left an id the WebUI
   re-fetched forever, one 404 per poll per orphan. Adds
   cleanupOrphanQueueEntries to the idle housekeeping pass: entries with
   no task file are dropped and logged. Self-heals existing strays.

   (Provoked by my own clean-up of two test tasks earlier in this
   session, but the gap is real and predates it.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:48:55 +01:00
librelad
2d1e4aa98f feat(health): self-healing control-plane watchdog + crash-loop failure cap
An offline trivy install crash-looped (server FATALs when it can't fetch the
vuln DB), and on rootless docker the restart storm churned the shared network's
port-forwarder until the WebUI's own published host port was torn down — the
WebUI stayed healthy INSIDE its container but was unreachable from the host, with
nothing detecting or healing it.

Three fixes, in the house self-healing style (mirrors the network-drift trio):

1. Control-plane health checker wired into the existing task-processor idle poll
   (maybeRegenPoll), no new daemon. dockerHealthScan (read-only) detects daemon
   down, a WebUI running-but-host-port-unreachable (the port-forward corruption),
   and crash-looping containers. webuiSystemHealthCheck writes
   frontend/data/system/health_status.json + self-dispatches a heal — the user
   can't click a button on a dead WebUI, so the poll drives the fix. Frontend
   health-notifier surfaces a topbar badge + dashboard banner + details panel.

2. Failure cap, enforced centrally by dockerHealthHeal (task-gated): stops
   crash-loopers (removing the churn), restarts the WebUI to re-publish a lost
   port forward, and — only if that fails — recycles the rootless daemon and
   restarts the core container. Caps every app immediately, no template churn.

3. Trivy no longer crash-loops offline: the server runs in a shell retry-loop so
   the container stays Up and quietly retries on a backoff instead of exiting
   FATAL. Verified: container stays Up across repeated DB-download failures.
   Core WebUI compose gains restart: unless-stopped so it self-recovers after a
   reboot / daemon recycle instead of staying down.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-17 22:03:02 +01:00
librelad
cfb3a4f755 revert: drop the dedicated Marketplace section — keep catalog merged into the grid
Reverts a4e65df. The maintainer prefers the more fluid model where registry
apps flow into the App Center grid as 'Available — Add' cards alongside
installed/local apps, rather than a separate /apps/marketplace destination.
Restores the Stage-4 merged-grid behaviour (loadApps merges
registry_catalog.json; createAppCard renders registry cards; addRegistryApp
dispatches app_add). The make_app.sh featured passthrough + the generator's
featured/source fields go with it (unused by the merged grid).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-04 21:50:57 +01:00
librelad
a4e65df77f feat(webui/marketplace): dedicated Marketplace section in the App Center
Splits the catalog out of the main grid into its own destination, the
WordPress 'Add Plugins' vs 'Installed Plugins' model: the grid is now
purely 'your apps' (local definitions), and the new Marketplace section is
'get more apps' (the remote signed catalog).

- New MarketplacePage (components/apps/marketplace/) mounts at
  /apps/marketplace inside the apps feature (same sub-dispatch pattern as
  /apps/overview — no new top-level component). Pinned sidebar entry with a
  live 'available to add' count badge.
- Status strip: signed/unsigned, available + catalog counts, serial, source
  base+channel, freshness, and a Refresh that re-runs the host-side registry
  scan via updater_check.
- Publisher-curated Featured shelf (meta.featured, set at publish time — no
  tracking/popularity), category chips + search, per-app detail modal
  (long description, publisher/trust/version, add command), and the chained
  Add & set-up flow: dispatch app_add, and when the definition lands, hand
  off to the app's config/install page.
- State-aware cards: Available (Add) / Added (Set up →) / Installed (Open).
- Backend: make_app.sh passes through meta.featured; webui_registry_scan.sh
  emits featured + source{base,channel} in registry_catalog.json.
- Removed the grid's registry-merge + registry card path + its CSS (moved to
  the namespaced marketplace surface); app_add task wiring + completion
  handler retained and reused.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-04 21:38:12 +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
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
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
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
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
librelad
1a4de624d0 refactor(webui): separate core/css into base/components/screens
core/css held 13 flat stylesheets. Grouped by role, matching the JS sub-system
split:

  core/css/base/        foundation (3): tokens, style, themes
  core/css/components/  chrome + widgets (7): topbar, sidebar, modal, forms,
                        config, update-notifier, aurora-background
  core/css/screens/     full-page screens (3): login, loading-screen,
                        setup-wizard

Path-only move (git mv) + href rewrite in index.html. No @import anywhere and
the <link> line order is unchanged, so the cascade is preserved; all 13 paths
verified to resolve.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 19:39:14 +01:00
librelad
aa563f1fed refactor(webui): separate core/lib + core/boot into sub-system folders
core/lib held 18 loose .js; core/boot held 10. Grouped by responsibility,
mirroring the components/* sub-system layout:

  core/lib/task/    task kernel (8): event-bus, commands, actions, router,
                    global-functions, manager, refresh-coordinator,
                    parameter-preserve
  core/lib/config/  config helpers (2): config-shared, config-options
  core/lib/util/    generic utils (7): data-loader, dom-helpers, ui-helpers,
                    lp-ui, router, system-live, dismissible
  core/boot/setup/  first-run wizard (3): setup-detector, setup-wizard,
                    setup-completion-watcher
  core/boot/controls/ form widgets (2): custom-number, custom-select

core/lib is now purely subfolders. backup-app-card.js was misfiled in lib (it's
a UI card) — moved to core/ui/. core/boot keeps the genuine bootstrap singletons
at root (system-loader, system-orchestrator, auth-manager, loading-ui,
theme-registry).

Path-only moves (git mv); all 35 referenced core paths verified to resolve,
full node --check sweep of 100 frontend .js files clean.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 19:32:09 +01:00
librelad
939f0223fb refactor(webui): separate backup component into sub-system folders
backup/ held 15 loose .js plus css/html at the component root. Split into
per-tab sub-systems mirroring the admin/apps layout:

  backup/core/{js,css,html}/   schema, base BackupPage, fetch-client,
                               cron-schedule, backup.css, backup-content.html
  backup/dashboard/js/         backup-dashboard
  backup/snapshots/js/         backup-snapshots, backup-snapshot-actions
  backup/locations/js/         backup-locations, backup-location-fields,
                               backup-location-modal, backup-ssh-key
  backup/migrate/js/           backup-migrate
  backup/configuration/js/     backup-configuration, backup-retention-presets,
                               backup-engine-details

Updated the scripts[] array + loadFragment in index.js, the backup.css href in
index.html, and the handleBackup() fallback paths in spa.js. No behaviour
change — files moved verbatim (prototype-augment clusters), all 17 referenced
paths verified to resolve, node --check clean.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 19:18:43 +01:00
librelad
f896df6006 refactor(admin): move bare css/ into admin/core/ for sub-system consistency
admin had sub-system folders (config/overview/system/ssh/peers) AND a bare
root css/ (admin.css) — inconsistent. A component's shared base belongs in a
core/ sub-folder once it has sub-systems (matching apps/core/), so admin.css
moves to admin/core/css/admin.css. Audit confirms all components are now
consistent: sub-system components (apps, admin) use core/ + sub-systems;
single-page components (backup/dashboard/tasks/updater) stay flat js/css/html
(no sub-systems, so no core/ needed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 18:26:31 +01:00
librelad
2ef4cc00e1 refactor(webui): granular sub-system folders per component
De-clutter each component into sub-system folders (apps: core/ port-manager/
services/ tools/ routing/; admin: config/ overview/ system/ ssh/ peers/) with
the standard js/ css/ html/ icons/ layout inside; single-page components
(backup/dashboard/tasks/updater) get js/ css/ html/. Single-feature icon sets
moved into their sub-system (vpn -> apps/core/icons, config/cpu/os ->
admin/{config,system}/icons); shared app + category icons stay in core/icons.
feature.json + index.js stay at each component root (the scanned descriptor +
entry). Every controller/CSS/fragment/icon path reference rewritten; verified
no stale refs, all JS valid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 12:42:35 +01:00
librelad
474c518df1 docs: fix index.html comment to reference components/ after the reorg
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 07:43:58 +01:00
librelad
d39852aa3d refactor(webui): reorganize into components/ + core/ taxonomy
Final modularization layout (user-chosen): every page is a self-contained
folder under components/<id>/ (controllers + CSS + its html fragment), and all
shared/framework code folds into core/:
  core/kernel  (feature-registry, lifecycle, services, spa)
  core/boot    (auth, system-loader/orchestrator, setup, loaders)
  core/lib     (data-loader, router, helpers, the task kernel, shared modules)
  core/ui      (topbar, modal, notifications, … + topbar.html)
  core/css     (all shared stylesheets)
  core/icons
Top level is now just: components/, core/, themes/, index.html (+ runtime data/).

Every path reference rewritten (index.html, scripts arrays, fetch()/
loadFragment()/loadScript() literals, system-loader + config-manager controller
paths, kernel manifest URL, feature.json, backend FEATURES_DIR). The
/api/features/list endpoint NAME is unchanged (it now scans components/).
Deleted 3 dead files (app-content.html, apps-content.html, html-cache.js).
Verified: 0 stale prefixes, 0 double-rewrites, all JS/JSON valid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 07:13:52 +01:00
librelad
e1794069cb feat(webui): add App Updater feature (versions, CVEs, disaster recovery)
New self-contained feature in features/updater/ (mirrors the backup feature):
- index.js + feature.json (auto-discovered; routes /updater + sub-tabs).
- updater-page.js: 5 tabs — Overview (update/CVE/recovery counts), Updates
  (per-app current->available + Update/Update-all), Security (CVEs by severity,
  links to NVD), Recovery (per-app rollback points; snapshot-before-update),
  History. Reads /data/updater/generated/{updates,cves,history}.json; falls
  back to the installed-apps list so it's useful before the first scan. All
  actions route through services.tasks (updater_check/apply/apply_all/rollback)
  — no new mutating API.
- updater.css (self-contained, teal --page-updater hue) + updater-content.html.
- New topbar 'Updates' nav button (nav-updater) + active-highlighting in
  topbar.js + spa.js. Kernel: setupRoutesFromManifest now allows module-only
  features (no legacy handler) — this is the first such feature.

Backend generator + 'libreportal updater' task land next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 02:45:40 +01:00
librelad
eaafd1bb38 refactor(webui): relocate admin area into features/admin/ + shared extractions
- features/admin/: the 10 admin-owned config controllers, the 5 admin pages
  (overview/system/charts/metric/storage), ssh-page.js, peers-page.js, plus
  admin.css/ip-whitelist.css/ssh.css (eager). config-manager.js kept last in
  the load order (it news the sub-managers).
- shared/js/: config-shared.js + config-options.js (ConfigShared/ConfigOptions
  globals consumed cross-feature by backup/apps/tasks).
- shared/css/: forms.css + config.css (generic form + config-form primitives
  borrowed by apps/backup/admin).
- Updated all path strings in system-loader.js (config component) and
  config-manager.js (lazyLoad of admin/ssh/peers controllers); index.html CSS
  hrefs. No /js/components/{config,admin,ssh,peers}/ refs remain.

js/components/ now holds only shared UI (topbar, notifications, eo-modal,
update-notifier, mobile-menu, confirmation-dialog).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 02:10:09 +01:00
librelad
ee44a4eb80 refactor(webui): relocate backup into features/backup/, app-card to shared/
- features/backup/: backup-schema.js, backup-page.js, backup.css (eager).
- shared/js/backup-app-card.js: cross-feature (used by backup AND the
  app-detail Backups tab), so it goes to shared/, not buried in backup.
- Updated paths: feature scripts array, spa.js handleBackup fallback,
  system-loader apps-manager component (app-card), index.html backup.css href.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 02:06:31 +01:00
librelad
f15cfe3043 refactor(webui): relocate apps + app-detail controllers and CSS into features/apps/
Move the 6 app controllers (apps-manager, app-tabbed-manager, port-manager,
routing-manager, services-manager, tools-manager) and their 7 stylesheets
(apps, apps-layout, tools, services, service-buttons, routing, port-manager)
into features/apps/. JS paths only existed in system-loader.js (the
apps-manager + app-tabbed-manager components); CSS hrefs in index.html (kept
eager). The cross-feature task-manager (/shared/task/) + backup-app-card
entries in the apps-manager component are untouched here. Globals unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 02:03:56 +01:00
librelad
b4649cd713 refactor(webui): relocate tasks page + shared task kernel
- features/tasks/: tasks-manager.js (the /tasks page controller) + tasks.css.
- shared/task/: the 6 cross-cutting task-kernel files (event-bus, commands,
  actions, router, global-functions, manager) + task-refresh-coordinator.js —
  used by tasks AND apps/app-detail/backup, so they go to shared/, not a
  feature. task-parameter-preserve.js stays at js/ (shared root).
- Updated all path strings: system-loader.js task-system + apps-manager
  components, apps-manager loadTaskSystem(), index.html (refresh-coordinator +
  tasks.css). Globals (taskEventBus/tasksManager/TaskManager/...) unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 02:00:59 +01:00
librelad
3abc45985a refactor(webui): relocate dashboard into features/dashboard/
Move dashboard.js + dashboard.css into the feature folder (kept eager-linked
from the new paths in index.html — dashboard.js stays eager because
system-loader calls its bare globals at boot). No reference sites outside
index.html (the feature's index.js uses globals, not the controller path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 01:56:12 +01:00
librelad
98d950ba44 feat(webui): phase 2 — DI service container (ctx.services)
Introduces kernel/services.js (window.LP.services): an additive, typed,
LAZY view onto the existing cross-cutting singletons — tasks{bus,refresh,
route}, live, auth, data, notify, theme, modal, router. It constructs
nothing (pure getters onto the live globals), so there's no double-init and
the globals stay authoritative. MountContext now injects it as ctx.services.

Slot names/globals were verified against the real code (workflow map): the
design doc's §4 list was wrong in several places — no window.taskManager
(client slot dropped), tasks.route lives on tasksManager.router, auth has no
status(), DataLoader isn't a window prop (lexical fallback), modal/router are
split surfaces (grouped/bound objects).

Migrated the 4 cross-cutting refs in the feature modules onto ctx.services
(admin: router.adminCategoryFromPath + tasks.refresh; backup: tasks.refresh;
app-detail: router.appPath). Page-owned controllers stay feature-globals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 00:46:57 +01:00
librelad
0724ed785a feat(webui): load feature modules from the manifest (drop index.html script list)
The kernel now loads each feature's self-registering index.js from the
manifest (new 'module' field) before building routes, so index.html no
longer hardcodes a per-feature <script> list — one of the four registries
the modularization targets is now gone. Adding a page = drop a
features/<id>/ folder + a manifest entry; no index.html edit.

loadScript is idempotent and non-fatal: a module that 404s or fails to
register leaves its route on the legacy handler. Manifest-load failure
still falls back to the built-in route table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 23:52:51 +01:00
librelad
b4105d8cff feat(webui): migrate the Admin area to one feature module
features/admin/index.js owns all /admin* sub-routes (overview, config/<cat>,
system[/sub], tools/ssh-access, tools/peers). mount() parses the category and
delegates to the system-loader configManager singleton's renderConfig();
unmount() stops AdminSystem's live SSE sub + 30s interval, drops the
admin-overview task-refresh registration, and nulls the per-visit
sub-controllers (adminOverview/adminSystem/sshPage/peersPage) while leaving
configManager intact. Legacy /config /ssh /peers redirect handlers unchanged.

With this, every WebUI page now routes through the feature-module kernel;
the legacy handleX() methods remain only as fallbacks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 23:37:51 +01:00
librelad
247310f370 feat(webui): migrate App Center + app-detail to feature modules
- features/apps/index.js (/apps*) and features/app-detail/index.js (/app*)
  as two features; /apps* registered first so wildcard precedence holds.
  Both drive the system-loader-pre-initialized singletons (appsManager /
  appTabbedManager) via .initialize(), mirroring the legacy handlers exactly
  (incl. app-detail's legacy ?app=/?=name parsing + ?tab=/?config= rewrite).
- kernel/lifecycle.js: ctx.nav(path, addToHistory) so app-detail's empty-name
  redirect matches navigate('/apps', false) exactly.

unmount is a no-op for both (shared singletons); the app-tabbed-manager
listener-rebind leak is pre-existing and handled in a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 23:35:14 +01:00
librelad
e6e796311a feat(webui): migrate Dashboard + Tasks to feature modules
Two more pages on the feature-module contract (specs produced + adversarially
verified by workflow):
- features/dashboard/index.js: landing page; mount() folds in the data-reload
  that used to be a navigate() special-case (now deleted from spa.js, so it
  fires exactly once). No controller class — uses the eager dashboard.js globals.
- features/tasks/index.js: re-inits the system-loader tasksManager singleton;
  unmount() clears the 30s auto-refresh interval + open log streams WITHOUT
  stopping the shared SSE bus or nulling the singleton.

Verifier fixes applied: deleted the duplicate dashboard reload in navigate();
dropped a dead detachDashboardLive() call; fixed an invalid try{}while(false)
in the tasks unmount.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 23:29:31 +01:00
librelad
182be8c33d feat(webui): phase 3 (first feature) — migrate Backup to a feature module
Introduces the kernel lifecycle and migrates the first real page to the
feature-module contract:
- kernel/lifecycle.js: MountContext (loadScripts/loadFragment/setContent
  + an AbortController/unsub teardown ledger so mounts can't leak
  listeners or live streams).
- features/backup/index.js: Backup Center as a self-contained module
  (LP.features.register with mount/unmount); heavy backup-page.js stays
  lazy-loaded on first mount.
- spa.js: routes whose feature has a registered mount() are driven
  through the kernel; everything else still uses its legacy handleX().
  navigate() unmounts the current feature first. Both fall back to the
  legacy handler if a module is missing or mount throws.

Strangler step: /backup now flows manifest -> registry -> mount/unmount.
The other pages are untouched. handleBackup remains as the fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 23:02:24 +01:00
librelad
57c17647e3 feat(webui): phase 1a — shared base token layer (tokens.css)
First slice of the per-module CSS strategy: introduce
shared/css/tokens.css as the always-present, theme-agnostic base token
layer, loaded before all other stylesheets.

- Defines --font-mono (ui-monospace stack). Several feature sheets used
  var(--font-mono, monospace) with no definition, falling back to bare
  monospace; this unifies them with the richer stack used elsewhere.
- Hoists the --page-* identity hues out of css/admin.css so they belong
  to the base layer rather than the admin stylesheet. Values unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 22:49:39 +01:00
librelad
2eaa5857a1 feat(webui): phase 0a — feature-module kernel scaffold (passive)
Adds the foundation of the feature-module architecture
(docs/frontend-modularization.md) as inert, additive code:
- kernel/feature-registry.js: window.LP.features — runtime register(),
  manifest loader, route-table + nav builders.
- features/manifest.dev.json: hand-committed manifest mirroring spa.js
  setupRoutes() exactly (route -> handler + navId).
- index.html loads the kernel before spa.js.

Zero behaviour change: nothing consults the registry yet. Phase 0b flips
routing to be registry-driven with the spa.js Map as fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 22:28:19 +01:00
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
9a92805bdb feat(ui): Beginner/Advanced experience level + linked dev mode + setup-wizard step
Adds the install-time Beginner/Advanced choice the user described, with
the linked dev-mode escape hatch and global body-class machinery that
any surface can hang advanced/dev-only DOM off.

Three-tier mental model, two flags in the data model:

  Beginner            default. nothing extra shown.
  Advanced            .lp-advanced DOM revealed; advanced wizard steps shown
  Adv+Dev             .lp-dev DOM also revealed; dev-only fields visible

Linking rule (enforced inside LpUi):
  - enabling dev auto-enables advanced (dev w/o advanced is incoherent)
  - disabling advanced auto-disables dev

Wire shape:
  CFG_INSTALL_LEVEL                  beginner | advanced (general_basic)
  CFG_DEV_MODE                       existing, unchanged behaviour
  window.LpUi.{advanced,dev}         {get(), set(), apply()}
  localStorage keys                  lp.ui.advanced, lp.ui.dev, lp.ui.seeded
  body classes                       lp-ui--advanced, lp-ui--dev
  events                             lp-ui-advanced-changed, lp-ui-dev-changed
  global CSS gates                   body:not(.lp-ui--advanced) .lp-advanced { hide }
                                     body:not(.lp-ui--dev) .lp-dev { hide }

Setup wizard:
  - New step 1 "Choose your experience" with Beginner/Advanced cards.
    Beginner is preselected so race-through gets the safe default.
  - Picking a level updates totalSteps live (4 for beginner, 5 for
    advanced) so the progress bar reflects the choice.
  - Metrics step (Prometheus + Grafana) is gated to Advanced — beginner
    never sees it, never gets asked, never installs them by accident.
  - Submit payload now carries install_level; setup-routes.js validates
    it against the enum (beginner|advanced).
  - scripts/setup/setup_apply.sh writes it to CFG_INSTALL_LEVEL via
    updateConfigOption.
  - On submit, LpUi.advanced.set is called immediately so the next
    surface (running-tasks page) is already in the right mode — no
    refresh needed.

WebUI bootstrap:
  - js/utils/lp-ui.js loads first thing in index.html (before any other
    bootstrap) so body.lp-ui--advanced is applied pre-paint — no FOUC
    of advanced content on a fresh tab.
  - On first run, seeds lp.ui.advanced from CFG_INSTALL_LEVEL.
    Subsequent loads honour the user's per-browser override.
  - Mirrors CFG_DEV_MODE → lp.ui.dev on the seed pass.

Dev-mode unlock:
  - Existing 10-click LibrePortal-logo easter egg unchanged.
  - NEW: same 10-click unlock on the Advanced toggle (in services-manager).
    Reuses the countdown-toast pattern; on the 10th click delegates to
    the topbar's _setDevMode so there's one canonical setter and the
    config_update task path stays singular.
  - TopbarComponent now exposes its instance as window.topbar so the
    toggle's tap handler can reach _setDevMode.
  - topbar._setDevMode also calls LpUi.dev.set(enabled) so the body
    class flips immediately (no reload needed to see dev-only DOM).

Convention rolled out:
  - Services tab's .service-rich panel was already gated on
    body.lp-ui--advanced.
  - .lp-advanced / .lp-dev are now first-class hide classes any
    component can tag DOM with — see style.css globals.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 23:31:50 +01:00
librelad
9f7ad8f177 feat(system): live 1 Hz SSE stream behind admin gauges + dashboard tile
Adds /api/system/stream — a Server-Sent Events feed driven by a single
per-process ticker that reads /proc directly and splices in the latest
host-side metrics.json each second. Subscribers share the connection so
N open tabs cost one ticker, and the ticker pauses entirely when nobody
is listening.

Frontend gets a singleton LiveSystem EventSource manager with auto-
reconnect, Page-Visibility integration (closes on tab hide), and last-
sample replay for late subscribers. Admin -> System gauges and the
dashboard memory + disk tile now tick at 1 Hz; trend charts and the
per-app table keep their 30 s poll because the underlying files only
regenerate once a minute.

Also adds /api/system/history as a thin range-query wrapper over the
existing 24 h JSON ring buffer — the binary ring backend will slot in
behind it in the next phase without changing the response shape.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 20:17:58 +01:00
librelad
d123eda869 perf(webui): defer page-specific scripts to first navigation (Phase B)
7 page-specific controllers were eager-loaded in index.html on every cold
visit, even when the user lands on /dashboard and never opens /backup,
/admin, etc. Moved them to lazy-load via spa.js's existing loadScript()
helper, fired from each route's handler on first navigation:

  /js/components/backup/backup-page.js       — handleBackup()
  /js/components/backup/backup-app-card.js   — handleBackup()
  /js/components/ssh/ssh-page.js             — config-manager ssh-access
  /js/components/peers/peers-page.js         — config-manager peers
  /js/components/admin/admin-overview.js     — config-manager overview
  /js/components/admin/charts.js             — config-manager overview
  /js/components/admin/admin-system.js       — config-manager system

config-manager.js gets a tiny `lazyLoad` helper that delegates to
window.spaClean.loadScript with a graceful fallback when the SPA hasn't
booted (legacy paths). loadScript is idempotent — subsequent visits to
the same route are no-ops, so we don't re-fetch after the first nav.

Cold-load impact on /dashboard (the most common landing):
  Before: 25 sync <script> tags loading ~1.7 MB raw / ~430 KB gzipped
  After:  18 sync <script> tags loading ~1.5 MB raw / ~380 KB gzipped
  + corresponding parse-cost reduction on the client (no longer parsing
    backup-page.js + apps-related JS just to render the dashboard)

Page-specific JS still loads cleanly when the user navigates there — a
single extra network round-trip per route on first visit, then cached
for 1h (per Phase A's cache headers). Compression (Phase A) means the
deferred JS is ~75 % smaller on the wire than it would have been
pre-Phase-A.

Sister update to .../Scripts/update.sh: rsync now uses --delete so
file removals in the source tree (this commit deletes 7 script tags;
earlier commits deleted config-manager-old.js) propagate to the live
install. Excludes still protect frontend/data/.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 22:25:36 +01:00
librelad
1014dd6e42 feat(peers): introduce 'Peer' as a first-class concept (Phase 2)
A peer is a named reference to another LibrePortal instance. Phase 2 only
implements kind=backup-channel (friendly label over a hostname that shows
up in a shared backup repo); direct-ssh-direct and direct-ssh-via-relay
(Connect's blind-relay) are reserved enum values for Phase 3.

DB schema (db_create_tables.sh):
  CREATE TABLE peers (
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
    name         TEXT UNIQUE NOT NULL,
    kind         TEXT NOT NULL DEFAULT 'backup-channel',
    config_json  TEXT NOT NULL DEFAULT '{}',
    status       TEXT DEFAULT 'unknown',
    last_seen    TEXT,
    created_at   TEXT DEFAULT CURRENT_TIMESTAMP
  );
  + indexes on name and kind.

  config_json is kind-specific so new transports don't need a schema
  migration. For backup-channel it carries {"hostname":"","loc_idx":N}.

Bash module (scripts/peer/):
  peer_helpers.sh   _peerDb, peerSqlEscape, peerValidateName/Kind.
  peer_add.sh       peerAdd <name> <kind> [k=v ...] → INSERT, refresh
                    generator. Rejects unimplemented kinds early so users
                    don't create dead-end peer records.
  peer_remove.sh    peerRemove <name> → DELETE.
  peer_list.sh      peerList → JSON array; peerGet, peerNameForHostname
                    (reverse-lookup for the migrate-tab overlay).
  peer_check.sh     peerCheckReachable, peerCheckAll. For backup-channel
                    'reachable' = at least one snapshot from that hostname
                    visible in (preferred|any enabled) location. Updates
                    status + last_seen so UI dots render without re-probing.

CLI (scripts/cli/commands/peer/):
  libreportal peer list
  libreportal peer get <name>
  libreportal peer add <name> backup-channel hostname=<host> [loc_idx=<n>]
  libreportal peer remove <name>
  libreportal peer check [name]

  Auto-routed by cli_initialize.sh's category-discovery.

WebUI data generator (scripts/webui/data/generators/peers/webui_peers.sh):
  Emits data/peers/generated/peers.json with the peerList output and a
  generated_at envelope. Hooked into webuiLibrePortalUpdate alongside the
  backup generators.

Frontend:
  - New top-level /peers route in spa.js (PeersPage class, peers-content.html).
  - 'Peers' nav item in the topbar between Backups and the right-side controls.
  - Add-peer modal with friendly-name + kind + hostname + preferred-location
    selector (populated from the existing backup-locations data).
  - Per-peer card with status dot, last-checked time, Check + Remove buttons.
  - Phase 3 kinds appear in the kind dropdown as disabled options so users
    can see what's coming.

Source-array wiring:
  - generate_arrays.sh auto-created files_peer.sh from the new peer/ dir.
  - cli_files.sh + app_files.sh include ${peer_scripts[@]} alphabetically.
  - files_webui.sh auto-picked-up the new peers/ generator subfolder.

The migrate-tab friendly-name overlay (use peer names in /backup/migrate
when a peer record exists for a hostname) is intentionally deferred — it's
a 5-line frontend lookup once peers.json is loaded; cleaner to add after
Phase 3 ships its peer-detail view.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 17:43:56 +01:00
librelad
62f7a84126 feat(webui): Admin System page with gauges, trend charts & per-app stats
New 'System' admin page (sidebar Tools group) rendering the metrics the
collector now produces:
- live ring gauges for CPU, memory, disk and load
- SVG trend charts (CPU/mem/disk/network) with 1h/6h/24h range toggle
- host info + swap + docker summary strips
- per-app table: CPU/mem bars, network, status, CPU sparkline

Charts are hand-rolled SVG in charts.js (LPCharts) — no third-party libs or
CDN calls — themed entirely from the active theme's CSS variables. The
Overview System card now links here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 16:47:20 +01:00
librelad
b5107e30cc feat(admin): Admin Overview landing + unified Admin page headers
Add an Admin Overview as the Admin landing (default when you open Admin): an
ops/health board distinct from the user Dashboard. Four cards built from data
we already generate — Updates (update_status.json, with one-click update),
Backups (backup dashboard.json), SSH & Security (access.json), System
(disk/memory/system_info) — each with a Manage link into the right section.
Styled like the backup dashboard (tiles/status dots).

Wire-up: 'Overview' is the top sidebar item and the default category
(handleConfig + sidebar), rendered by AdminOverview into #config-section via a
renderConfig('overview') special case. Every Admin page now shows the same
'Admin' breadcrumb header (Overview, SSH Access, and the config categories) for
a consistent Admin → Section feel. User Dashboard gets an 'Admin overview →'
link.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 17:57:21 +01:00
librelad
e75f10618d feat(ssh): WebUI SSH Access page
New /ssh page (topbar nav + SPA route + SshPage controller + ssh-content.html
+ ssh.css). Reads data/ssh/access.json and lets the admin: paste a public key
to authorize a machine, remove keys, and toggle key-only login — all via
'libreportal ssh ...' tasks through the backend's lockout guards. Reuses the
backup key-card styles for a consistent look. This is the inbound counterpart
to the backup location key card (outbound): same paste-a-key model, opposite
direction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 16:52:47 +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
d5fe1bc56b feat(webui): out-of-date detection + one-click update
Surface when LibrePortal is behind upstream and let users update from the
WebUI, reusing the proven git-update path instead of reinventing it.

Detection (host): webuiSystemUpdateCheck writes
frontend/data/system/update_status.json from a throttled git fetch +
behind-count + VERSION compare, off the existing per-minute
`webui generate system` cron. A new /VERSION file is the canonical version.

Display (frontend): update-notifier.js/.css render a global topbar badge
(every page) and a dashboard banner (prominent when behind, subtle "up to
date" with a manual check otherwise), plus a details panel.

Actions go through the task pipeline:
- `libreportal update apply` -> webuiRunUpdate (non-interactive: guards,
  forced check, gitPerformUpdate, then dockerInstallApp libreportal)
- `libreportal update check` -> forced recheck

gitFolderResetAndBackup's body is extracted into gitPerformUpdate (no exit)
so the WebUI path can reuse it; the interactive CLI flow is unchanged.

Detection JSON verified against the repo (up-to-date and behind cases).
webuiRunUpdate's re-clone + redeploy still needs validation on a live host.

The latest-version source is git for now and is the single swap point for
get.libreportal.org later — the JSON contract and frontend stay unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-21 23:33:43 +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