1117 Commits

Author SHA1 Message Date
librelad
ce8170c6ad Merge claude/2 2026-06-25 21:23:24 +01:00
librelad
f1d22b057a fix(webui/updater): friendlier Updates empty-state copy
'No installed apps to track.' was terse and gave no next step. Point the user
to the App Center so the empty Updates tab is actionable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 21:23:24 +01:00
librelad
712068a00e Merge claude/1 2026-06-25 21:18:23 +01:00
librelad
dac824a161 fix(webui/forms): enhance per-app config & port dropdowns with themed select
The custom-select enhancer only matched select.form-control /
select.theme-selector, so dropdowns rendered by the per-app config form
(config-form.js uses form-select / form-input / config-input), the app
tools form, the port-manager grid (port-* classes) and the instance
domain picker stayed as plain native OS dropdowns. The LibrePortal app's
Theme option is one of these.

Add those classes to ENHANCE_CLASSES, give the classless instance domain
select a form-control class, and add a compact button override so the
themed dropdown matches the dense port-manager input metrics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 21:18:15 +01:00
librelad
4e90328b84 Merge claude/2 2026-06-25 21:17:31 +01:00
librelad
754864571c fix(webui/updater): friendlier, shorter improvements empty-state copy
'No hotfix data yet — the automatic scan fetches the signed improvements index
within a couple of minutes.' leaked jargon (hotfix data, signed index) onto the
interface. Replace with a short, plain 'Checking for improvements…' line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 21:17:31 +01:00
librelad
e054cbc946 Merge claude/2 2026-06-25 21:07:07 +01:00
librelad
f4c24340b7 fix(webui): lighten faint empty-state messages on dark panels
Empty-state 'no data yet' messages rendered at rgba(text, 0.55) (and one at
text-muted), which is hard to read on the dark recessed panels. Switch them to
the theme's --text-secondary muted token so they stay de-emphasized but legible.

Covers .updater-empty, .updater-detail-empty, .sys-detail-empty, .eo-modal-empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 21:07:07 +01:00
librelad
209ced11be Merge claude/2 2026-06-25 20:54:42 +01:00
librelad
246e687e45 fix(webui/tasks): equalize task-list panel top/bottom padding
The last task tile's 10px bottom margin stacked on the list's 16px bottom
padding, leaving a larger gap below the last row than above the first.
Zero the last tile's bottom margin so the dark panel reads symmetric.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 20:54:42 +01:00
librelad
b1f935dc3b Merge claude/1 2026-06-25 13:26:51 +01:00
librelad
c48f9f4ffc fix(webui/storage): drop redundant 'Reclaiming space…' toast
Clicking Reclaim space fired two notifications: routeAction → executeTask
already shows the rich 'Reclaim Space task started!' toast (icon + bold
LibrePortal + task link), then _reclaim added a second, plain, iconless
'Reclaiming space…' info toast on top of it. The image-removal path doesn't
double-notify like this. Drop the redundant one — the start toast plus the
completion toast give clean feedback on their own.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 13:26:51 +01:00
librelad
d605eb788c Merge claude/1 2026-06-25 13:24:01 +01:00
librelad
23712bd0c4 fix(webui/system): stop loading/empty overlays covering populated metric graph
.sys-detail-loading and .sys-detail-empty are absolutely-positioned overlays
that JS shows/hides via the `hidden` attribute. Their .sys-detail-* rule sets
`display: flex`, an author declaration that overrides the UA
`[hidden] { display: none }` — so `el.hidden = true` never actually hid them.
Both the 'Loading history…' and 'No samples in this range yet' overlays stayed
painted on top of a fully-populated chart (overlapping into garbled text).

Add `.sys-detail-loading[hidden], .sys-detail-empty[hidden] { display: none }`
(higher specificity than the bare class) so the hidden attribute wins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 13:24:00 +01:00
librelad
93c5076225 Merge claude/2 2026-06-25 13:17:14 +01:00
librelad
9850b9d8e7 fix(admin/storage): make the Storage page tablet/mobile friendly
The donut legend collided into the stat cards and the image-row details
were squished on tablet widths — the 220px sidebar leaves the content
cramped while the old breakpoints assumed full-viewport width.

- Headline collapses to a single column at <=1024px (was 800px), the two
  stat cards reflow side-by-side, and on phones the donut stacks above its
  legend with one stat per row. Legend labels now ellipsis instead of
  overflowing into the stats.
- Image rows group the name+pill and the size/shared/age metadata so the
  metadata drops onto its own line under the name at <=1024px instead of
  competing for width; on phones the Delete button collapses to an icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 13:17:14 +01:00
librelad
39ccb08119 Merge claude/2 2026-06-25 12:56:08 +01:00
librelad
e33701ee52 feat(admin/storage): filter images by in-use/unused, in-use first
The Images list on /admin/system/storage now has an All / In use / Unused
segmented filter (with live per-group counts), and the default All view
sorts in-use images to the top — the ones you can't reclaim lead, the
reclaimable ones follow. Select all / Clear All act on the visible rows,
so they honour the active filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 12:56:08 +01:00
librelad
36e2368d54 Merge claude/1 2026-06-25 12:54:39 +01:00
librelad
c7484572df fix(webui/tasks): give app-less task notifications the LibrePortal identity
App-less system tasks (verify, regen, …) resolved to an empty displayName
and null icon in _taskNotificationDescriptor, so their completion toast
rendered an empty <strong></strong><br> — a blank bold line that showed as
a random gap above the message — and had no icon, unlike every other
notification. Treat any task with no app slug as a system task so it gets
the 'LibrePortal' subject and libreportal.svg icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 12:54:39 +01:00
librelad
c66eb78671 Merge claude/1 2026-06-25 12:50:36 +01:00
librelad
be427e5376 fix(webui/admin): balance top/bottom padding in sys-tasklist
The .sys-tasklist panel spaces rows with a flex gap, but each .task-item
also carries margin-bottom: 10px from the shared tasks styles. That margin
only stacks on the last row, so the list had ~8px above the first row and
~18px below the last row, looking lopsided. Zero the row margin inside the
list so spacing is symmetric.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 12:50:36 +01:00
librelad
bff71d0feb Merge claude/2 2026-06-25 12:47:10 +01:00
librelad
10ce8a1453 style(docker/rootless): trim tombstone comment in rootless user setup
Describe only the current useradd behaviour; drop the narration of the old
silent-failure bug (per the repo's no-tombstone-comments convention).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 12:47:10 +01:00
librelad
1f02156318 Merge claude/2 2026-06-25 12:43:50 +01:00
librelad
3c27adb337 refactor(docker/rootless): just ensure slirp4netns via apt
Drop the GitHub-release version comparison entirely. We install slirp4netns
from apt regardless, so comparing against the GitHub-latest tag only produced
a perpetual 'outdated' loop and a no-op re-install. apt-get install -y is
already idempotent, so run it unconditionally and report the resulting
version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 12:43:50 +01:00
librelad
46eb1bedfd Merge claude/2 2026-06-25 12:41:57 +01:00
librelad
0f844783a3 fix(docker/rootless): parse slirp4netns version cleanly
slirp4netns --version prints multiple lines (version, commit, libslirp,
SLIRP_CONFIG_VERSION_MAX). The old 'awk {print $2}' ran on every line and
also picked the literal word 'version' from line 1, producing a multi-line
blob that leaked into the 'is outdated' notice. Read only the first line and
take field 3 (the actual number), strip the leading v from the GitHub tag so
the comparison is meaningful, and skip the check if the tag fetch fails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 12:41:57 +01:00
librelad
8fdbe1ea08 Merge claude/2 2026-06-22 14:58:54 +01:00
librelad
c1df1aef40 fix(webui/services): box the Services loading state like other tabs
The Services tab's loading placeholder (.services-loading) was a bare
centered row with no container chrome, unlike the Config and Tasks tab
loading cards. Give it the same boxed look (semi-opaque black fill,
hairline border, rounded corners, margin and min-height) so it reads as
a deliberate loading panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-22 14:58:54 +01:00
librelad
cf80d48126 Merge claude/2 2026-06-22 14:57:06 +01:00
librelad
85e5920afe fix(webui/apps): app-detail tab clicks no longer snap back to config
showAppDetail() derived the target tab from the legacy ?tab= query
(searchParams.get('tab')), but the app is path-based now
(/app/<name>/<tab>), so that read was always null and defaulted to
'config'. Since loadTabContent() calls showAppDetail() on every switch,
clicking any non-config tab (services/backups/updater/tasks) immediately
rewrote the URL back to /app/<name> and rendered config.

Read the current main tab off the path via appPartsFromPath, honouring
it only when already on this app; cross-app/cold nav still starts at
config. The legacy ?tab= shape is already normalised to the path by the
SPA's handleAppDetail before this runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-22 14:57:06 +01:00
librelad
c961e7cb11 Merge claude/2 2026-06-22 14:41:37 +01:00
librelad
70f16ef1e3 fix(webui/tasks): auto-expand opens one row, not all of them
autoExpandTask (the monitorTask path) opened its row directly without
collapsing the others and never set highlightedTaskId — unlike every
other opener (toggleTaskDetails, selectTask), which enforce a single
open row. So a burst of monitored task creations, e.g. a multi-app
first install, stacked every panel open at once.

Wait for the row to render, then delegate to selectTask, which collapses
any other open panel, sets highlightedTaskId, attaches the right log
view (live stream vs snapshot) and scrolls into view. Setting
highlightedTaskId also makes monitorTask's own guard trip after the
first task, so the running-task auto-follow takes over from there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-22 14:41:36 +01:00
librelad
38a7e4c365 Merge claude/2 2026-06-22 14:30:28 +01:00
librelad
370f05921a fix(rootless): don't start docker before its network override is written
Initial rootless setup ran 'systemctl --user start docker' immediately
after install, but the rootless net/port-driver override.conf (and the
daemon-reload that loads it) aren't written until further down. So the
first start always failed — 'Job for docker.service failed' plus a
spurious '✗ Error Setting up Rootless' in the error report — even though
the later 'systemctl --user restart docker' brought the daemon up fine
once the override was in place.

Drop the premature start from the install step (keep install + enable);
the restart after the override is written is now the first real start.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-22 14:30:28 +01:00
librelad
10477a2651 Merge claude/2 2026-06-22 14:25:06 +01:00
librelad
8c81e8722c refactor(install): order initFolders by root, children grouped under each
Folder creation output interleaved the system/containers/backups roots
and their children. Regroup the array so each root is immediately
followed by its own children (alphabetical), keeping parents before
children since the mkdir has no -p.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-22 14:25:06 +01:00
librelad
d842ed8447 Merge claude/1 2026-06-21 23:00:47 +01:00
librelad
655dbc2bb9 fix(install): restore webui_logins container-group after credential write
The rootless WebUI container reads its bind-mount sources (configs/webui/*)
through the container-owner GROUP since a2376e2 switched those files from
world-readable to 0640 group=container-owner. But the WebUI credential
randomizer rewrites webui_logins via `sed -i` as the non-root manager, which
recreates the file with the manager's own group — dropping the container-owner
group. The installer then started the container immediately, so node hit
EACCES on /app/webui_logins at require-time (parseConfigFile) and exited 1;
nothing listened on the WebUI port. `libreportal webui login reset` had the
same latent bug (rewrite → restart). Under the old world-readable model a
post-sed file stayed o+r so the container could still read it, which is why
this only surfaced on fresh rootless installs after a2376e2.

Fix: make reconcileWebuiDirOwnership the single "ready the WebUI for its
container" pass — it now also restores the configs/webui bind access (new
`webui-bind` ownership action) on top of the container-dir chown. Reorder the
installer so the credential randomizer runs BEFORE the before-start permission
pass, making that pass the last ownership touch before the container starts;
and call reconcileWebuiDirOwnership before the restart in login reset.

Live box recovered via `libreportal-ownership reconcile`; WebUI 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-21 23:00:47 +01:00
librelad
38b3f189b8 Merge claude/1 2026-06-18 18:05:24 +01:00
librelad
a2376e2fc6 fix(security): webui config files reachable by group, not world
_webui_bind_access granted o+r to every file in configs/webui so the
rootless container could read its bind-mount sources — but that also made
secrets like webui_logins world-readable to any local user. Under rootless
the container's gid 0 maps to the container owner's gid, so group access is
sufficient: chown the webui dir + files to MANAGER:container-owner, dir
0751 (traverse, not list), files 0640. Container reads via group; other
local users get nothing; the manager (owner) still rewrites them.

Verified live: container READ ok, world READ denied, manager rw, WebUI
login still 200. Live helper updated in lockstep with this source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-18 18:05:24 +01:00
librelad
d458fa5ea4 Merge claude/1 2026-06-18 17:51:27 +01:00
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