1074 Commits

Author SHA1 Message Date
librelad
d522a19cae docs(roadmap): App Files tab proposal + UID-access spike results
Design note for a per-app Files tab scoped to LibrePortal-managed files
(not system files): four file buckets (hidden/view-only/editable/lever),
the advanced/dev mode as the single escalation lever (not per-file flags),
and the hard rule that the flag is UX-only while the locked-down task CLI
stays the security boundary (jail + secret allowlist).

Includes the live UID-access spike: the manager owns and can write the
config tree (/libreportal-system/configs) directly, but the container tree
(/libreportal-containers/<app>) is dockerinstall-owned — readable, not
writable — so config edits need no helper while compose-class edits do.
webui_logins is manager-readable, so secret-hiding must live in the CLI
allowlist, not in perms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-18 17:51:27 +01:00
librelad
da0d6bb6a5 Merge claude/2 2026-06-18 16:42:46 +01:00
librelad
0d5ae61e32 fix(app-config): restore config sub-tab on cold-load deep-link/refresh
/app/<name>/config/<sub> URLs (e.g. .../config/ports) are generated by the app
itself and shown in the address bar, but a refresh or deep-link always reset to
the first config category. Cause: showAppDetail() rebuilt the URL via
appPath(appName, targetTab) with NO sub argument and pushState'd it BEFORE
renderAppDetail() read the sub back off the path — so the /<sub> segment was
already gone and preferredCategory stayed null.

Preserve the sub when the URL already points at this app's config (matched by
appPartsFromPath().app === appName), so cold-load/refresh lands on the encoded
sub-tab. Cross-app switches still start at the first category. The sibling
showAppDetailWithConfig() (the grid 'manage' button) is intentionally left to
land on the first category.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-18 16:42:46 +01:00
librelad
f4784b5fc1 Merge claude/1 2026-06-18 16:15:15 +01:00
librelad
75162af648 fix(webui): Services-tab Advanced toggle reveals rich detail again
The per-service rich detail panel (.service-rich: limits, healthcheck,
networks, mounts) and the live mem chip both did `const fmt =
window.SystemFmt` and bailed when it was absent. SystemFmt is defined by
the lazy admin System page module, so on the app Services tab it's usually
undefined — `_renderRichDetail` returned '' and the panel was never in the
DOM, so flipping Advanced revealed nothing.

Give the Services component its own SVC_FMT formatter (mirrors SystemFmt)
and use `window.SystemFmt || SVC_FMT` everywhere, dropping the hard bail.
The rich panel now renders whether or not /admin/system was ever visited.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-18 16:15:15 +01:00
librelad
fb9c1ee14a Merge claude/2 2026-06-18 16:04:07 +01:00
librelad
626041a39e style(ports): use the shared lp-ui-advanced-toggle for 'Show advanced fields'
The port manager's 'Show advanced fields' control was a raw <input type=
checkbox> + <span>, out of step with the modern pill switch the Services tab
uses for its Advanced toggle. Swap the markup to the shared .lp-ui-advanced-
toggle (track + thumb) structure — defined in services.css, which is loaded
globally — keeping .port-manager-show-advanced on the input for the JS hook.
Drop the now-dead .port-manager-advanced-toggle CSS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-18 16:04:06 +01:00
librelad
4e17346dfb Merge claude/2 2026-06-18 15:45:58 +01:00
librelad
0fbbde1000 style(app-config): wrap each config field in a translucent card
App-config fields rendered as a loose vertical stack with no grouping. Give
each .app-config .form-field the .admin-card glass recipe (var(--card-bg) fill,
faint rgba(text,0.10) hairline border, 12px radius, 16px padding) so each
option reads as a discrete container. Spacing now comes from the .panel-fields
flex gap, so the per-field margin-bottom is dropped to avoid doubling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-18 15:45:57 +01:00
librelad
c9a6847e48 Merge claude/2 2026-06-18 15:32:46 +01:00
librelad
bbf2f9a5f4 style(overview): match sidebar Overview entry to app-category rows
The pinned 'Overview' sidebar entry used a rounded, inset pill (margin +
border-radius:8px) so its hover/active highlight floated in the middle of the
sidebar, unlike the full-width app-category rows below it. Drop the margin and
radius, adopt the .category padding (15px 20px), border-bottom separator and
var(--surface-hover) hover, so the highlight spans the full sidebar width with
square corners and matches the categories. Keeps font-weight 600 + the
page-updater active tint as its only distinguishing marks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-18 15:32:46 +01:00
librelad
9a19e55533 Merge claude/1 2026-06-17 18:49:06 +01:00
librelad
01961e5bb9 fix(webui): tasks list panel hugs its content instead of overhanging
The recessed task-list box had flex:1, so its background filled the full
height and ran well past the last task. Move the scroll onto .tasks-terminal
and let .tasks-list size to its content, so the box ends at the last task
(and still scrolls when the list overflows). Scrollbar styling follows to
the new scroll container.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-17 18:49:06 +01:00
librelad
2cf4ce1495 Merge claude/1 2026-06-17 18:45:57 +01:00
librelad
c02202d620 fix(webui): stop Backups-tab card bg from running past the footer buttons
On /overview Backups the card surface lived on .main, which wraps both the
body and the flipped footer — so the background overhung past the action
buttons. Move the card surface onto .backup-page-body (rounded bottom,
joined to the tab strip) and let the footer sit transparent below it,
matching the app Config tab's .tabs-content + .config-actions split.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-17 18:45:56 +01:00
librelad
9a58869899 Merge claude/2 2026-06-17 18:40:40 +01:00
librelad
0bcde854e6 refactor(webui): move fleet Overview under /apps/overview; retire standalone /backup
The fleet Overview area (Overview/Updates/Improvements/Backups/Migrate) now
lives at /apps/overview* instead of /overview*, reflecting that it belongs to
App Center. The Backups tab is therefore /apps/overview/backups, and the old
standalone Backup Center page is removed entirely:

- apps feature owns /apps/overview* (covered by the existing /apps* route); its
  mount() dispatches /apps/overview -> fleet Overview before the grid check.
- _legacyRedirect() rewrites old short URLs so bookmarks/links keep working and
  the address bar shows the canonical path:
    /overview[/tab] -> /apps/overview[/tab]
    /backup[/sub]   -> /apps/overview/backups[/sub]
    /updater, /peers redirects retargeted to /apps/overview*
- Removed the standalone backup feature: components/backup/{index.js,feature.json},
  its manifest entry, the /backup route registrations and the dead handleBackup().
  The BackupPage classes stay — the Overview Backups tab embeds them.
- Repointed every backups/overview link: the admin dashboard's 'Open backup
  center', the app-card 'Open backup center' button + snapshot-overflow link,
  the sidebar Overview entry, the improvements deep-link, and the Migrate
  'go to locations' deep-link.

Also drop the redundant inline Check button from the Security empty state
(same rationale as Improvements: the host auto-scan repopulates it and the
header carries a manual Check).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-17 18:40:40 +01:00
librelad
de5621746d Merge claude/1 2026-06-17 18:36:25 +01:00
librelad
82325bce43 style(webui): wrap /admin/system body in the recessed admin panel
Apply the same recessed-panel treatment as the Dashboard/Tasks to the
System page: gauges, Trends, Storage, Host and Per-app now sit inside one
dark rounded box under the header divider. Generalise the Dashboard's
.admin-card-grid-wrap into a shared .admin-panel class so both admin pages
use one source of truth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-17 18:36:25 +01:00
librelad
168a8f25f5 Merge claude/1 2026-06-17 18:23:50 +01:00
librelad
0641a9b790 style(webui): wrap admin dashboard cards + task list in recessed panel
Match the fleet Overview's .ov-tab-body treatment: the /admin/dashboard
card grid (under the header divider) and the /tasks list now sit inside a
recessed dark rounded box instead of floating directly on the page
gradient.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-17 18:23:49 +01:00
librelad
9653c33931 Merge claude/2 2026-06-17 17:57:26 +01:00
librelad
830d361351 fix(overview): drop redundant Check button from empty Improvements state
The Improvements tab's empty state ('No hotfix data yet …') rendered an
inline 'Check now' button. It was redundant: the embedding Overview header
already carries a manual Check, and the host-side auto-scan repopulates the
signed improvements index on its own within a couple of minutes (the empty
message already says so). Remove the button so the empty state is just the
self-explanatory, automation-backed message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-17 17:57:26 +01:00
librelad
6f8596fc88 Merge claude/2 2026-06-17 17:30:36 +01:00
librelad
3653a39fd8 chore(config): quiet per-file reconcile output, drop backup note
The config reconcile pass printed one 'Reconciled config: <name>  (backup:
.<name>.bak)' line per changed file. Drop the per-file message entirely:
the intro notice and the two per-section '...completed.' confirmations are
enough, and the backup mention added noise. The hidden .<file>.bak sibling
is still written for safety — it's just no longer announced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-17 17:30:36 +01:00
librelad
a4fc1f7c14 Merge claude/2 2026-06-12 23:39:28 +01:00
librelad
168924757e fix(tasks): reap orphaned running tasks immediately at processor startup
Holding the singleton flock at startup proves no other processor is alive
to heartbeat or complete anything, so every task still marked running is
a corpse from a killed predecessor. Recover them all before the first
dispatch (recoverOrphans now takes an 'all' mode) instead of waiting out
the 60s heartbeat-staleness window — which used to leave a dead task
showing 'running' alongside the genuinely-running next task for a minute
whenever the service was restarted mid-task (e.g. by the deploy chain
during initial setup). The idle-loop pass keeps the stale-only gate.

refactor(dashboard): slim the storage card back to chart + percentage

The disk card was only ever meant to be the donut and the % figure; drop
the Apps/Docker/Other/Free legend rows and signal the deeper view with a
corner expand glyph instead (the System page's chart-expand icon) — the
card already opens /admin/system/storage on click.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 23:39:28 +01:00
librelad
0d10284203 Merge claude/1 2026-06-12 23:26:41 +01:00
librelad
a28eed0729 fix(services): route per-service restart through the task system + CLI
The Services tab restart button POSTed to a backend endpoint that (a)
checked the app's compose path from INSIDE the webui container, where
the host's containers root isn't mounted — so every restart failed with
'Compose file not found' — and (b) queued a raw 'docker compose restart'
that the host task processor would run as the manager user, which can't
talk to the rootless daemon anyway. Errors surfaced via a bare alert().

Per-service restart now follows the exact shape of the whole-app verbs:

- CLI: 'libreportal app restart <app> [service]' — the optional service
  arg makes dockerRestartApp restart just that compose service, via
  dockerCommandRun (right user in rootless mode) from the app dir on the
  host, where the compose file actually lives. Service names validated
  against compose-legal characters before touching a shell line.
- WebUI: the button dispatches a 'service_restart' task action through
  the task router (mutations-via-tasks), runs in the background with the
  standard task toast + link — no page switch — and failures use the
  notification system instead of alert(). Because the task runs host-
  side, restarting the WebUI's own libreportal-service now works too.
- Backend: the mutating restart endpoint and its now-unused helpers are
  removed; service-routes.js is read-only surface (status + log tails).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 23:26:40 +01:00
librelad
105644364f Merge claude/1 2026-06-12 23:11:14 +01:00
librelad
87e19e197a fix(config): hide reconcile backups as dot-named siblings; guard the option resolver
Reconcile backups now land as .<file>.bak instead of <file>.bak, so they
no longer clutter the configs folder. The .bak suffix is kept, so every
existing walker/sourcing exclusion still applies.

Also exclude dotfiles and *.bak from findConfigFileForOption: it walked
the configs tree with no backup exclusion, so depending on directory
order a 'config update' could resolve a key to the backup file and write
the user's change there — silently lost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 23:11:14 +01:00
librelad
1e997f75d2 Merge claude/1 2026-06-12 22:33:55 +01:00
librelad
e317962616 Merge claude/2 2026-06-12 22:33:23 +01:00
librelad
7c28007779 refactor(config): updater knobs -> configs/webui/webui_updater; fix config heal/reconcile gaps
Move the WebUI-updater settings out of general_terminal into their own
advanced webui-category file (webui_logs precedent): new
configs/webui/webui_updater holds CFG_UPDATER_SCAN_INTERVAL and the
migrated CFG_HOTFIX_AUTO, listed in webui/.category.

The move only reaches existing installs if the config convergence
machinery works, and three pieces of it silently didn't:

- checkConfigFilesMissingFiles walked a stale hardcoded category list
  ('general features network' — features doesn't exist; webui/backup/
  security never healed). Derive the categories from the template tree
  instead, and heal .category metadata too: copy it when absent and
  merge missing SUBCATEGORY_ORDER entries when present, so healed files
  actually appear in the WebUI Config editor. core_categories removed.
- Option reconciliation never touched ANY nested config file: configs_dir
  carries a trailing slash, so rel stripping missed ('configs//'), the
  template lookup failed, and reconcileConfigFile early-returned for
  every file. Strip the slash before matching.
- reconcileConfigFile's AUTO_DELETE=false branch read a never-populated
  live_line array, losing the dropped keys it promised to keep. Populate
  it alongside live_value.

Also exclude *.bak from config sourcing (reconciliation writes <file>.bak
next to live configs — now that it runs, sourcing backups would resurrect
deleted keys), and add 'libreportal config check' as a non-interactive
front door to the converge pass (was only reachable via install flows and
the interactive menu).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 22:33:23 +01:00
librelad
7db2a707b2 refactor(overview): turn the fleet Overview tab into an action board
Replace the stat-tile grid with a needs-attention board: a health hero on
top (one big status circle — green all-clear / amber something-to-do /
neutral pre-scan — matching the admin dashboard's dot language) over one
status row per area (updates, security, improvements, backups). Rows that
want a decision are tinted with their area hue and carry their action
buttons inline (Review / Update all / Open Backups); healthy areas
collapse to a quiet neutral one-liner. An Everything / Needs action chip
pair filters the board down to just the actionable rows.

Board rows deep-link with intent: Security lands on the Updates tab
pre-filtered to the affected apps via a new data-filter hop on the goto
action. backupSummary() now splits never-backed-up from week-stale apps
so the backup row can say which it is.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 22:33:23 +01:00
librelad
5b437c0c52 Merge claude/1 2026-06-12 22:07:42 +01:00
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
86f84a62d3 Merge claude/1 2026-06-12 19:47:41 +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
c913be9808 Merge claude/2 2026-06-12 19:41:35 +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
0e8f645334 Merge claude/2 2026-06-12 18:55:10 +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
86abfadb52 Merge claude/1 2026-06-12 18:31:18 +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
12a37cc734 Merge claude/2 2026-06-11 18:27:06 +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
dfd4eb0f17 Merge claude/2 2026-06-05 00:21:02 +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
16dd146710 Merge claude/2 2026-06-05 00:17:30 +01:00