1249 Commits

Author SHA1 Message Date
librelad
9d84e1e918 Merge claude/2 2026-07-07 22:25:03 +01:00
librelad
41786edd36 fix(backup/webui): show the retention "Backup style" divider only for Custom
.backup-retention-preset-block carried a permanent border-bottom (+ padding),
but the Keep-* fields it separates the dropdown from are hidden for every
preset except Custom — so on Self-hosting/etc. it was an orphaned line with
empty space beneath it. Moved the divider + spacing onto an .is-custom state
that applyVisibility() toggles alongside the fields.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 22:25:03 +01:00
librelad
34102eb6db Merge claude/1 2026-07-07 22:05:57 +01:00
librelad
d41073fad7 fix(webui/tasks): quiet the noisy task toasts for background (self-surfacing) tasks
A background task like the updater check surfaces its result in the page it was
launched from (the Updates/Overview tab repaints itself), yet it still fired the
full "View Task" pair — "Check task started!" then "Check task completed!" —
on top of the launcher's own small "Checking apps for updates…" line. Three
notifications for something you have no reason to open the Tasks page for.

Add a small, generic classifier (LP_BACKGROUND_TASKS) keyed by task action/type
(with a command regex that also catches the backend `updater check auto` run).
For a classified task:
  - executeTask() skips the standard "task started!" toast and, when the run is
    hand-launched, records its id as pending.
  - the taskCompleted handler skips the standard "task completed!" toast; if the
    id was pending (user launched it) it shows one small plain result line
    ("Apps checked for updates & vulnerabilities."), otherwise stays silent so
    the periodic auto-scan makes no noise at all.

Non-background tasks are unaffected — the standard task-notification style still
applies. Extend byAction to quiet another self-surfacing action.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-07 22:05:56 +01:00
librelad
e6d2eec141 Merge claude/2 2026-07-07 21:55:12 +01:00
librelad
143c1ca23d fix(config/webui): align multi-column config toggles inline with inputs
In the config grid (e.g. Backups > Configuration > Engine) toggle pills sat
~6px below and 2px taller than the text/select controls beside them, so they
read as misaligned. Two causes, both scoped to .config-fields:

- .field-group carried a vestigial margin-bottom:6px (the grid's own 16px gap
  already spaces rows). It inflated the grid track, so the align-self:end
  toggle landed 6px below the input controls. Zeroed inside the grid.
- The .checkbox-label pill's 24px switch + 10px vertical padding rendered 46px
  vs the inputs' 44px. Trimmed to 9px padding so the pill is 44px.

Toggles now share the exact vertical band (top + height) as the inputs in
their row. Applies to every config-grid toggle, not just the backup engine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:55:12 +01:00
librelad
01e8743a8e Merge claude/1 2026-07-07 21:40:57 +01:00
librelad
ada5a4c177 fix(webui/config): box the master-toggle control row like the rest of a section
On /admin/config/general the Basic section's fields sit in the darker inset
".config-fields" box, but the Mail Enabled toggle and Installation Mode select
did not — their control sits in its own .mail-master-toggle / .git-master-toggle
wrapper, a sibling of .config-fields, which the boxing rule never covered. So a
gated section's enable row read as loose, unboxed content above the (hidden)
options.

Extend the boxed-panel rule to the master-toggle wrappers (mail / install-mode /
generic / backup-remote). Element-qualified to the wrapper <div> because
renderToggleSection stamps the same class on the inner .checkbox-label pill too,
which must not get its own nested box.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-07 21:40:57 +01:00
librelad
17075274c2 Merge claude/2 2026-07-07 21:40:23 +01:00
librelad
64d298c6db fix(catalog/webui): make the official-catalog "Re-enable" button inline, full-height & clickable
When the official catalog is hidden, its "Re-enable" action rendered as a
flat, greyed-out ~27px pill pinned to the top-left of the row — shorter than
the 44px URL field and not obviously clickable (the generic .btn-secondary
loses its nebula-theme polish outside .config-actions, so it fell back to an
unstyled base button).

- The action column now align-self:stretch, so its control matches the URL
  field's height and sits inline (also vertically centres the compact × in the
  enabled state).
- The Re-enable button gets a dedicated .catalog-reenable-btn treatment: accent
  fill + accent border + hover state + an eye icon, filling the row height so it
  lines up with the input and clearly reads as a button.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:40:23 +01:00
librelad
126f563a07 Merge claude/2 2026-07-07 21:17:08 +01:00
librelad
6bed124984 fix(backup/webui): hold Refresh spin ~450ms so instant refetches still pulse
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:17:08 +01:00
librelad
ac07420ef1 Merge claude/2 2026-07-07 21:13:06 +01:00
librelad
6912c36aaf fix(backup/webui): render the Backups Configuration form (dup-id collision) + refresh feedback
The Backups tab's Configuration sub-tab showed only the warning banner and
an orphaned divider -- the actual config form (retention presets, engine,
keep-policies, Save/Reset) never appeared.

Root cause: a duplicate id. The Fleet Overview, the app grid and per-app
detail all render the shared apps-unified-layout, which keeps a hidden
#config-tab > #config-section in the DOM. The Backups Configuration sub-tab
created its OWN #config-section, and configManager.renderConfig() resolved
the target with getElementById('config-section') -- which returns the FIRST
(hidden) node in document order. So the 21KB form painted into the invisible
per-app container while the backup tab's container stayed empty. Because the
/backup routes now redirect into /apps/overview/backups, this fired every
time -- the form had effectively never been visible.

Fix:
- renderConfig(category, target) now accepts an explicit container (element
  or id) and falls back to #config-section; the error-retry button re-targets
  via configSection.id.
- The Backups Configuration sub-tab uses a unique id (#backup-config-section)
  and passes it to renderConfig; retention-preset and engine-details
  selectors are scoped to it.

Also: the Refresh button now spins + locks while refreshAll()+render() run,
so the click has visible feedback instead of appearing to do nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:13:06 +01:00
librelad
b33764f259 Merge claude/2 2026-07-07 20:47:21 +01:00
librelad
6148a6597e fix(backup/webui): wrap the global Backups tab list in a themed box
The global Backups tab rendered its snapshot list (and the "No backups
yet." empty state) as a bare flex column directly on the tab panel, so
the list section had no visual container -- unlike the Dashboard tab's
cards and the per-app Backups tab's recessed panel. Wrap it in a
.backup-snapshot-panel box (translucent fill + hairline border, matching
.backup-card) so the section reads as a bounded surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:47:03 +01:00
librelad
95fc2f73e9 Merge claude/1 2026-07-07 20:46:46 +01:00
librelad
73a93cf145 fix(webui/backups): sync fleet Backups sub-tab URLs (/apps/overview/backups/<sub>)
The embedded backup center deep-links *in* fine (mountBackupCenter parses the
sub-segment), but clicking a sub-tab never wrote the URL: BackupPage disables
pushTabToUrl() when embedded and nothing on the fleet side stepped in, so the
address bar stayed stale and the sub-tabs weren't shareable/deep-linkable —
unlike the sibling Migrate sub-tabs.

Give BackupPage an onTabChange hook fired on every switch; the OverviewManager
supplies it to replaceState /apps/overview/backups/<sub> (replaceState, matching
Migrate, so the on-entry deep-link re-sync can't stack a duplicate history
entry). Standalone /backup keeps using pushTabToUrl() unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-07 20:46:45 +01:00
librelad
ff711d3c2b Merge claude/1 2026-07-06 23:25:46 +01:00
librelad
8aae1bde4f fix(webui/config): nudge the "Official" catalog badge inline with the URL text
The badge centres on the input box (top:50%), but the single-line input renders
its text a hair low, so the pill read as slightly above the URL. +2px lands it
optically inline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-06 23:25:46 +01:00
librelad
6b83fa34ad Merge claude/2 2026-07-06 23:13:55 +01:00
librelad
d571018b9f fix(catalog/webui): replace the official-hide warning wall-of-text with a ✓/✗ checklist
Feedback: the removal warning was an overwhelming paragraph and made it look like
updates might break. Keep the official catalog removable, but present the effects
as a clear two-part list instead of prose:
  ✓ Still works: system updates & hotfixes, installed apps, re-enabling any time
  ✕ What changes: official apps hidden from the App Center "Available" list
Backed by a new optional messageHtml arg on the shared confirmation dialog (dev-
controlled HTML body) + styles for the effect groups. Required tickbox kept.

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 23:13:55 +01:00
librelad
2c67b9b622 Merge claude/1 2026-07-06 23:03:56 +01:00
librelad
fb0c3ba3fb feat(config): split core-component toggles into a "Core" advanced section
The General → Install section was cluttered with 8 CFG_REQUIREMENT_* dev toggles
(config mgmt, CLI, WebUI, task service, database, passwords, Docker CE, Docker
Compose) — all **ADVANCED** **DEV** "which core components to install" switches.
Move them into a dedicated general_core section ("Core", advanced) so Install
keeps just the install-mode / release / git settings.

- New configs/general/general_core (**ADVANCED** header → advanced group, 🧩 icon).
- Removed those fields from general_install.
- .category SUBCATEGORY_ORDER: general_core after general_install.
Var names unchanged, so all install-requirement logic still resolves them. Live
install migrated in place (values preserved) + configs.json regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-06 23:03:56 +01:00
librelad
86d1c34e39 Merge claude/1 2026-07-06 22:47:10 +01:00
librelad
e0e13f7566 fix(webui/config): proper acronym/brand casing in config section names
formatSubcategoryName title-cased naively, so section headers read "Ssh",
"Dns", "Libreportal". Add a whole-word acronym/brand map so they render
professionally as "SSH", "DNS", "LibrePortal" (also covers VPN/API/IP/UI/URL/
HTTP(S)/TLS/SSL/WebUI for future files). Composes with stripCategoryPrefix,
which runs after and is case-insensitive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-06 22:47:10 +01:00
librelad
431d6561a8 Merge claude/2 2026-07-06 22:44:55 +01:00
librelad
190c06ebce fix(catalog/webui): inline info note + official row shows its URL, height matches
- Info banner: drop the borrowed traefik warning banner (icon sat above the
  text, amber-warning styling) for a .catalog-note — icon inline with the text,
  neutral boxed look matching the content sections.
- Official row (#1): render the release-host URL in a read-only input (same
  element/height as the third-party rows) with the "✓ Official" badge overlaid,
  instead of short text — so it shows the URL and its box height matches the
  others. URL is read from the loaded config (CFG_RELEASE_BASE_URL).

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:44:55 +01:00
librelad
8e8856f5d0 Merge claude/1 2026-07-06 22:33:08 +01:00
librelad
9b32444475 feat(config): per-file icons before the config-section names
Config sections (Basic, Mail, DNS, Firewall, …) rendered as a bare name. Add a
small data-driven icon before each: config files now carry a `# @icon <emoji>`
directive in their header, the config generator surfaces it as the subcategory's
`icon`, and the config editor prepends it to the section <h3> (covers every
render path — regular/master/toggle/domains/catalogs/whitelist — since they all
drop displaySubcategory straight into the heading).

- webui_generate_configs.sh: parse `# @icon` (first 10 lines) → emit "icon".
- config-manager.js: prepend subcategoryData.icon to the section title.
- configs/**: added an @icon to all 23 base config files (backup/general/network/
  security/webui). The live install's config files get the same directive + a
  configs.json regen so existing installs show the icons too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-06 22:33:08 +01:00
librelad
06723e0995 Merge claude/2 2026-07-06 22:29:46 +01:00
librelad
3766f73654 feat(catalog/webui): pin official source #1 (removable via warning+tickbox) + box config sections
- Catalog block now shows the official catalog as a pinned row #1 (verified
  badge + its URL from the generated data). Removing it opens the shared
  confirmation dialog with a REQUIRED tickbox (generalized to accept custom
  checkbox text) — it only hides official apps from browsing; system updates are
  unaffected. Backed by a new CFG_CATALOG_OFFICIAL_ENABLED flag the scan honors.
- Trimmed the info-banner wording per feedback.
- Config option sections: the body (after the divider, before the buttons) now
  sits in a darker boxed panel (bg + border), matching the content-box look used
  elsewhere. Applies to regular grids + the block editors; scoped to the main
  config editor.

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:29:46 +01:00
librelad
4242159217 Merge claude/2 2026-07-06 22:08:29 +01:00
librelad
506a3cf7ca feat(catalog/webui): domains-style block editor for CFG_CATALOG_* sources
Adds the Catalog Sources block to the config editor (General → App Catalogs),
mirroring the domains block UI: add/remove up to 9 catalog URL slots, saved by
the generic config-form (any CFG_* input) → configUpdateBatch, which rebuilds
registry_catalog.json. Official catalog stays source #1 (not editable here);
third-party rows carry an "unverified" note.

- new catalog-manager.js (lean CatalogManager; reuses the domains block CSS,
  URL inputs, no DNS checks) + window.addCatalog/deleteCatalog globals.
- config-manager.js routes the general_catalogs subcategory to it.
- system-loader.js loads catalog-manager.js before config-manager.js.

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:08:29 +01:00
librelad
cf238ae644 Merge claude/1 2026-07-06 22:05:00 +01:00
librelad
c7d5de7a4f fix(webui/forms): stop the custom-select enhancer double-wrapping a dropdown
On the app-details Config tab, switching main tabs re-renders the config form
(showAppDetail → displayConfigForm runs on every tab switch). A re-render race
let the enhancer wrap a <select> that was already enhanced, nesting a second
.custom-select inside the first — so the dropdown showed twice, one on top of
the other (reproduced: CFG_<APP>_BACKUP_STRATEGY / _COMPOSE_FILE ended up with
two nested wrappers). The per-element ENHANCED symbol didn't catch it.

Harden the guard with two DOM-based checks (which survive any symbol loss/race):
skip a <select> that already carries the .custom-select-native class or already
sits inside a .custom-select wrapper — in both shouldEnhance() and the
constructor. Port-manager selects were unaffected (they regenerate their
container each render); this fixes the plain form-select fields.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-06 22:05:00 +01:00
librelad
02b74baa6f Merge claude/2 2026-07-06 22:01:23 +01:00
librelad
abf3a65c88 refactor(catalog): move sources into a config file (CFG_CATALOG_1..9), generate from it
Per the intended model: catalog sources should live in the config place (like
domains), and registry_catalog.json should be a purely GENERATED artifact
derived from them — not the source of truth. Replaces the earlier
$docker_dir/catalog/sources.json store.

- New configs/general/general_catalogs — CFG_CATALOG_1..9, one catalog base URL
  per slot ("url" or "url|channel"), domains-style. Official stays pinned as
  source #1 (derived from CFG_RELEASE_BASE_URL, not listed here). Slot N → source
  idx N+1 (stable id for the Add picker / `app add --source`).
- catalog_sources.sh now reads/writes those CFG vars (via updateConfigOption)
  instead of a JSON file; dropped catalogSourcesFile + the enable/disable toggle
  (presence = enabled; remove = clear the slot).
- configUpdateBatch regenerates registry_catalog.json when a CFG_CATALOG_* key
  changed — so pressing Save in the WebUI rebuilds the browse data.
- webuiRegistryCatalogScan is unchanged (still iterates catalogEnabledSources).

Verified: CFG_CATALOG_1/2 → sources at idx 2/3, empty slots skipped, url|channel
parsed, official pinned at idx 1.

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:01:22 +01:00
librelad
2ab1aa3850 Merge claude/2 2026-07-06 21:44:17 +01:00
librelad
c0382dfa4f perf(install): don't build the WebUI image twice on a fresh install
startPreInstall called installLibrePortalImageWebUI standalone AND then
installLibrePortalAppWebUI, whose app-install step 1 builds the same image
again — so a fresh install built libreportal-service twice back-to-back
(2nd mostly Docker-cache hits, but a redundant copyFolder + "this may take a
while" block). The standalone call only matters on a rebuild where the
app-install path SKIPS (libreportal already in the DB). Gate it on exactly
that: build standalone only when libreportal is already installed. Net: one
build in both cases (fresh → via app install; reinstall → standalone).

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 21:44:16 +01:00
librelad
53497a662f Merge claude/1 2026-07-06 21:00:02 +01:00
librelad
2c4752adab Merge branch 'main' into claude/1
# Conflicts:
#	scripts/source/files/arrays/function_manifest.sh
2026-07-06 21:00:02 +01:00
librelad
ba4dbc6e02 fix(tasks): stop the lazy-autoload stub from looping forever + add processor failsafes
Root cause of the "task loops eternally" bug: the lazy-autoload stub was

    fn() { source "$file"; fn "$@"; }

If the source fails — the real case: an app-install/deploy rsync briefly
removes-then-replaces a generator file while a setup task calls it — `fn` is
never redefined, so `fn "$@"` re-invokes the *stub*, which sources the (still
missing) file, which re-invokes the stub… A single setup finalize recursed
12,050 levels, flooding the task log and taking 82s before it happened to
recover when the file reappeared. A permanently-missing file would never
recover.

Fix (root cause): drop the stub before sourcing —

    fn() { unset -f fn; source "$file"; fn "$@"; }

so a failed source degrades to one "command not found" (rc 127) instead of
unbounded recursion. Regenerated function_manifest.sh (975 stubs, reformat
only — no function-set change).

Failsafes on the task processor (defence in depth, per request):
- FUNCNEST cap (TASK_FUNCNEST_MAX, default 1000) inside the task's eval
  subshell — any runaway recursion now aborts in milliseconds instead of
  spamming the log until the stack/disk gives out.
- Wall-clock cap (TASK_MAX_RUNTIME_SECS, default 7200s, 0=off) — the heartbeat
  watcher TERM→KILLs a task's process group once exceeded and marks it failed
  (distinct from a user cancel via a .timeout marker). Generous so real long
  installs/backups/migrations finish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-06 20:56:07 +01:00
librelad
6c19fa006b Merge claude/2 2026-07-06 20:45:33 +01:00
librelad
f2207448b5 style(install): trim the new MTU/gate messages to one short line each
Per feedback — shorten the install-failure error (drop the extra hint line) and
the rootless MTU notice.

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 20:45:32 +01:00
librelad
c92fcc9773 fix(install): don't record an app as installed when its container never started
An image-pull failure (e.g. the MTU-EOF black hole) left apps marked
installed+active with no container: the compose `up -d` failure was invisible
because (a) checkSuccess read $? after `_rc=$?`, always printing "✓ Started",
and (b) the exit code is dropped across dockerComposeUpdateAndStartApp →
dockerComposeUpdate → dockerComposeRestartAfterUpdate, and installApp never
checked it — so post-install integrations ran and set status=1 regardless.

- up_app.sh: restore $? to the compose exit before checkSuccess (both rootless
  and rooted) so a failed `up -d` is reported as an error + logged, not "✓".
- app_install.sh: after `up`, reality-gate on the app's compose project having
  at least one container (ps -a, so a slow-to-start container still counts).
  If none exists, print a clear failure, skip _appPostStartIntegrations (which
  is what records the app + sets status=1), and return non-zero.

Verified: the gate query passes for a running app (libreportal) and refuses
apps with no container (navidrome/ipinfo after their pulls EOF'd).

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 20:43:56 +01:00
librelad
a234bd2875 Merge claude/2 2026-07-06 20:38:11 +01:00
librelad
ffc7801762 fix(network): auto-detect path MTU so large image pulls don't stall with EOF
CFG_NETWORK_MTU=1500 was baked blindly into the rootless daemon uplink
(DOCKERD_ROOTLESS_ROOTLESSKIT_MTU) and every app's container network. On links
whose real path MTU is below 1500 (Qubes/NAT/VPN with PMTU discovery blocked),
image manifests + tiny images pull fine but large image LAYERS stall and reset
mid-blob with "httpReadSeeker: ... EOF" — apps silently fail to install. Probed
here: path MTU ~1328, Docker at 1500 → black hole.

- New scripts/network/network_mtu.sh: networkDetectMtu (don't-fragment ICMP
  ladder → largest standard MTU that gets through, 1500 when ICMP gives no
  signal) + networkEffectiveMtu (CFG_NETWORK_MTU: a number is verbatim, "auto"
  probes once and caches $docker_dir/.network_mtu) + networkRedetectMtu.
- CFG_NETWORK_MTU default 1500 -> auto. Rootless setup now writes the resolved
  MTU into the override (and re-detects per install); the per-app NETWORK_MTU_TAG
  uses the resolved value too. Explicit numbers still win.

Verified on this box: auto -> 1300; alpine (multi-MB) + the full ~100MB navidrome
image now pull to completion where they previously EOF'd. Applied live (override
1300, config=auto, cache=1300, rootless docker restarted).

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 20:38:11 +01:00
librelad
3c6e885fed Merge claude/1 2026-07-06 20:06:13 +01:00
librelad
c302ee2318 feat(containers/navidrome): use the real vinyl-record logo icon
Replace the placeholder equalizer with a faithful SVG recreation of Navidrome's
blue vinyl-record mark (the project ships only PNG logos, so this is a clean,
scalable redraw): grooved disc, white centre label + spindle hole, and the two
detached groove ends.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-06 20:06:13 +01:00