1203 Commits

Author SHA1 Message Date
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
librelad
4dc62d301f Merge claude/2 2026-07-06 19:49:59 +01:00
librelad
748fde25c0 fix(catalog): store sources.json in the state root, not under configs/
Every file under $configs_dir is treated as a config (some get sourced at
startup), so a JSON file there was executed as bash — "[name:…]: command not
found" on every config load. Move it beside the apps DB in the manager state
root ($docker_dir/catalog/sources.json): persistent, manager-owned, and never
config-scanned.

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 19:49:59 +01:00
librelad
b83f8c589d Merge claude/2 2026-07-06 19:39:21 +01:00
librelad
90a20e9e17 feat(catalog): multiple catalog sources — backend model + multi-source scan + CLI
First slice of multi-catalog ("taps") support. Today the App Center browses one
catalog (get.libreportal.org). This adds an ordered list of catalog sources and
teaches the browse scan to merge them.

- New scripts/catalog/catalog_sources.sh — the source list in its OWN file
  ($configs_dir/catalog/sources.json). Source #1 is ALWAYS the official catalog,
  synthesized live from CFG_RELEASE_BASE_URL/CHANNEL (can't be edited/removed,
  always pinned on top). Extra sources are stored as a small JSON array and are
  UNVERIFIED (trust=community). Helpers: catalogSourcesJson / catalogEnabledSources
  (priority order) / catalogSourceAdd|Remove|Toggle|List / catalogFetchCommunityIndex.
- webui_registry_scan.sh now walks catalogEnabledSources: the OFFICIAL source is
  still signature-verified (lpFetchIndexInto, unchanged trust path); third-party
  sources are fetched unverified. Apps are merged by slug into one card carrying a
  sources[] array in priority order (highest first = default). Trust/verified are
  taken from the SOURCE, never the artifact's self-claim, so a community index
  can't promote itself to "official". Icons still mirror same-origin from the
  official index only. registry_catalog.json gains top-level sources[] + per-app
  sources[]; the old source{} object + signed/serial are kept for back-compat.
- New `libreportal catalog source list|add|remove|enable|disable` + `catalog
  refresh` CLI (dynamic-routed). Mutations go through the task system (cliTaskRun
  "…" "catalog"), never a new mutating API.

Scope firewall: this governs APP BROWSE + ADD only. LibrePortal's own updates and
hotfixes still resolve from the official CFG_RELEASE_BASE_URL alone — lpFetchIndex
is untouched, so a third-party catalog can never become a system-update channel.

Next: `app add --source`, then the WebUI (domains-style block source manager +
Add-dialog source picker with the Official badge / unverified warning).

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 19:39:21 +01:00
librelad
5cd9e67128 Merge claude/1 2026-07-06 19:38:02 +01:00
librelad
9f6ebfa37a feat(containers): add Navidrome music streaming server
New self-hosted app following the standard container layout (config +
docker-compose template + icon), modelled on the Jellyfin media app:

- navidrome.config — media-category metadata, single webui port (random:4533),
  served on the `music` subdomain via Traefik.
- docker-compose.yml — deluan/navidrome:latest with a ./data volume (DB/config/
  cache) and a read-only ./music library mount; declares its SQLite DB
  (libreportal.backup.db) + data dir for live, zero-downtime backups; gluetun
  markers included so it can optionally be VPN-routed.
- navidrome.svg — brand-blue equalizer icon.

Auto-discovered by the container scan / apps.json generator, so it appears in
the App Center as an installable Media app. Like Jellyfin, the first admin
account is created in Navidrome's own web UI on first run, so no auth tools are
needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-06 19:38:02 +01:00
librelad
c5f75425ac Merge claude/1 2026-07-06 19:15:27 +01:00
librelad
fb8ebd7924 fix(webui/overview): paint Backups header immediately, confine spinner to content
Switching to the fleet Backups tab blanked the whole pane to a bare, headerless
lpLoadingBox() while the (script-heavy) backup center loaded, so it read as the
entire section reloading with no indication of where you'd landed — then the
header + content appeared at once when the fetch resolved.

Render the shared .ov-tab-header (💾 Backups + description + divider) up front and
drop the spinner into an .ov-tab-body beneath it, matching the per-app detail
tabs' header-first idiom (and the recent Admin board fix). The header stays put
and only the content area swaps in once BackupPage mounts.

The Migrate tab already does this — mountMigrate() injects its header and the
sub-tab strip is static, so its spinner is already confined to the Restore/Peers
sub-panel — so no change was needed there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-06 19:15:27 +01:00
librelad
1adc480099 Merge claude/1 2026-07-06 18:54:05 +01:00
librelad
0e33efb9e1 revert(setup): drop the "no apps selected" confirm popup
Removes the zero-app confirmation dialog added earlier today — it wasn't
needed. Zero-app installs proceed straight through again. The other setup
fixes (finalize rolls up per-app results, watcher gates the hand-off on
whole-group success, dev-mode auto-enable skipped during setup handoff)
are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-06 18:54:05 +01:00
librelad
35c37b5086 Merge claude/1 2026-07-06 18:31:16 +01:00
librelad
e3ec256265 fix(setup): stop "setup complete" from lying about a half-done install
Three coupled defects in the first-run wizard flow, all surfacing as
"it said complete but nothing was set up":

1. Zero-app installs sailed through. With no apps ticked, setup was just
   config+finalize, finished in seconds having installed nothing, and
   fast-forwarded to an empty App Center. Add a self-contained in-wizard
   confirm ("Install with no apps?") before submitting. Can't reuse the
   shared confirmation-dialog component — it isn't loaded this early in
   boot — so the dialog is rendered by the wizard itself and mounted on
   <body> to escape the aurora surface's FX-stacking rule.

2. finalize declared success unconditionally. It never inspected the
   per-app install tasks, so a failed app still yielded "your install is
   ready" + a redirect. Pass the setup group id to `setup finalize`; it
   now rolls up the group's app-install task results and logs a clear
   partial/failed verdict. The WebUI completion watcher gates the welcome
   toast + App Center hand-off on the whole group succeeding, not just on
   finalize completing — a failed app now keeps the user on the tasks page
   with an error toast instead of a false all-clear.

3. Setup task count was confusing (banner "of 2" while the page listed 3).
   On dev/git installs the topbar's dev-mode auto-enable raced the wizard's
   own CFG_DEV_MODE write and spawned a second, un-grouped config-update
   task mid-setup. Skip that auto-enable while a setup handoff is active
   (the wizard already persists CFG_DEV_MODE); it runs on the next load if
   still needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-06 18:31:15 +01:00
librelad
f4ba7ffe51 Merge claude/2 2026-07-06 18:19:58 +01:00
librelad
10846b120e perf(backup): fetch snapshots once per location, filter in jq — kill the O(apps×locations) restic storm
The backup-data refresh in the WebUI update chain shelled out to
`restic snapshots` once PER APP (dashboard, app-status) and once per
(host, app) (migrate) — every call a `sudo`+`restic` spawn that re-opens
and decrypts the repo. With N apps and L locations that's O(N·L) (app-status)
plus O(hosts·apps·L) (migrate) spawns on every update — the silent stall
after "Refreshed backup dashboard data...".

One `restic snapshots --json` already returns the whole repo, so fetch it
ONCE per location and derive the per-app / per-host / system views in jq.
Validated against a live restic 0.19 repo: the jq filter reproduces
`restic snapshots --tag <t> --host <h>` exactly (incl. empty-host = all
hosts, and newest = last), and count/latest extraction matches the old grep.

- dashboard: one primary-location fetch → all app tiles + system status
  (was N snapshot calls + 1).
- app-status: one fetch per location → filter per app (was N×L calls);
  also de-recursed into a flat app loop.
- migrate: filter the already-fetched per-location list (was a spawn per
  host×app). Fixes a latent bug too — `grep -oc` over single-line JSON
  always reported a count of 1; jq `length` is now accurate.

restic snapshot spawns per refresh drop from ~O(N·L + hosts·apps·L) to ~O(L).
`restic stats` (still run per location in locations + dashboard) is a
separate, non-multiplicative cost left untouched to preserve its semantics.

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:19:58 +01:00
librelad
c3315d9fbf Merge claude/2 2026-07-06 17:58:38 +01:00
librelad
e168496824 perf(gluetun): throttle the provider-list fetch off the WebUI-update hot path
webuiLibrePortalUpdate runs the per-app refresh hooks on every update (the
task processor fires it repeatedly), and gluetun's hook silently pulls a
~7MB servers.json from GitHub each time. The hook's stdout is captured by
`result=$($_hook)`, so on a slow link the update just sits with no output
right after "Generated apps-tools.json..." — it reads as a freeze.

- Throttle the fetch to once per CFG_GLUETUN_PROVIDERS_REFRESH_HOURS (24h
  default; 0 = manual-only). Direct callers (Tools refresh, install hook)
  pass GLUETUN_PROVIDERS_FORCE=1 to still get a guaranteed pull.
- curl --compressed (this JSON gzips ~7x) + --connect-timeout 15 so a slow
  or dead link shrinks/fails fast instead of stalling every update.
- Print "Refreshing <app> WebUI data..." from the updater loop (outside the
  output capture) so the step is visible instead of looking hung.

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 17:58:38 +01:00
librelad
b93aa78d9c Merge claude/2 2026-07-05 20:33:21 +01:00
librelad
6dd729d0ac fix(webui/admin): paint page header immediately, confine spinner to content
The Admin Dashboard (OverviewPage) and Admin System (SystemPage) boards each
blanked the whole content pane to a bare headerless lpLoadingBox() on init(),
then rendered the .page-header + panel only once data arrived. So clicking
either tab flashed a full-pane spinner with no indication of which page you'd
landed on, then everything appeared at once.

Extract the page header into a _headerHtml() helper on each page and render it
up front in both the loading state and render(). Now navigating to a board
paints its header (breadcrumb + title + description) instantly and the spinner
is confined to the .admin-panel beneath it — the header stays put and only the
content swaps in when the fetch resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-05 20:33:21 +01:00
librelad
d96a9646bc Merge claude/1 2026-07-05 20:25:21 +01:00
librelad
f4913422fb refactor(catalog): rename the 'marketplace' app to 'libreportal_catalog'
'Marketplace' read as a place to buy things; this is a free, self-hosted app
catalog, so rename the app to LibrePortal Catalog. Slug is libreportal_catalog
(underscore — the slug becomes a CFG_<SLUG>_ prefix and a bash identifier via
declare "${app_name}=i"; a dash would break install). Docker-facing names use
dashes (libreportal-catalog-service / hostname libreportal-catalog), declared
explicitly in the PORT config + compose, not derived from the slug.

- containers/marketplace/ -> containers/libreportal_catalog/ (+ .config, .svg, hook file)
- CFG_MARKETPLACE_* -> CFG_LIBREPORTAL_CATALOG_*, APP_NAME + TITLE + PORT_1 updated
- install hook fn marketplace_install_post_setup -> libreportal_catalog_install_post_setup
- served browse site reworded Marketplace -> Catalog, icon refs updated
- regenerated function_manifest.sh (autoload stub now points at the new file)

Not installed yet, so there is no live config/container/volume to migrate — the
cheapest moment to rename. The client-side registry ('View full page on the
marketplace') wording is a separate subsystem and left unchanged for now.

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:25:21 +01:00
librelad
d0168ca43e Merge claude/2 2026-07-05 19:48:23 +01:00
librelad
f8e1072d1e fix(de-sudo): skip runtime dockerinstall-password re-sync (twin of 9050a8c)
start_scan.sh runs updateDockerInstallPassword every system scan, doing
`sudo passwd $CFG_DOCKER_INSTALL_USER` via runSystem. Model A's scoped
sudoers grants only LP_HELPERS/LP_SYSTEM + run-as-install-user — not passwd
— so at runtime (manager, non-root) it fails exit 1 every scan. This is the
exact sibling of the updateDockerSudoPassword failure fixed in 9050a8c; that
guard was added to the manager/sudo user but the dockerinstall user was
missed, so error_report.log kept logging "Updating the password for the
dockerinstall user" on every scan.

The password is set at install (root path, startPreInstall →
installDockerRootlessUser) and the rootless docker user is driven by tooling,
not a password login, so the runtime re-sync is legacy + impossible under
de-sudo. Guard it to skip unless EUID 0, mirroring the sudo-pass fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-05 19:48:22 +01:00
librelad
caf90bfdfa Merge claude/1 2026-07-05 12:43:12 +01:00
librelad
868b0d3c24 fix(webui/setup): darken the setup-progress banner so it reads as a solid card
The setup-in-progress banner floats over arbitrary page content (the aurora
gradient, apps grid, task logs) but filled at rgba(--bg-rgb, 0.45) with no
backdrop blur, so the page bled through and it looked washed-out. Mirror the
.notification treatment already used for floating toasts: a near-solid panel
(0.85) over a frosted backdrop, plus a lift shadow. Stays theme-aware via
--bg-rgb (near-black on dark themes, near-white on light).

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 12:43:12 +01:00
librelad
e6df7be1ca Merge claude/2 2026-07-04 23:14:35 +01:00
librelad
ae39bba93f feat(marketplace/site): hash-route the views (#view=submit) — shareable + bookmarkable
The pins now update the hash so How it works / Submit / Submissions are
deep-linkable; the #<slug> app-focus link still works. One parseHash()
distinguishes them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-04 23:14:35 +01:00
librelad
d1caee0b54 Merge claude/2 2026-07-04 23:10:21 +01:00
librelad
01cd6c3f9d feat(marketplace/site): How it works, Submit an app + Submissions views
Turns the site into a small multi-view app. Three pinned nav buttons above
the search (App Center pin style) switch the main pane:
- How it works: the former footer text, expanded into cards (add an app,
  point your box here, how trust works, run your own).
- Submit an app: the PR-based flow (fork → drop-in app folder → PR → reviewed
  + signed → published with a community badge + your name). No uploads/accounts.
- Submissions: renders a static submissions.json (operator-published, like the
  catalog) — author names, status badges (pending/review/merged), PR links,
  a pending-count badge on the pin, graceful empty/loading states.

Category/search return to browse; #<slug> deep-link still focuses one app.
REPO_URL is overridable via <meta name=lp-repo>.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-04 23:10:21 +01:00
librelad
06164fedd4 Merge claude/2 2026-07-04 22:54:06 +01:00
librelad
a8ac2f6dce feat(release): make_app.sh re-emits meta.featured (dropped in the section revert)
The featured passthrough was removed when the dedicated-section commit was
reverted; the Featured sidebar row needs it. spec.json {"featured":true} now
sets meta.featured on the published app again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-04 22:54:06 +01:00
librelad
8746448cbc Merge claude/2 2026-07-04 22:50:05 +01:00
librelad
855d698486 feat(marketplace/site): Featured sidebar row + match App Center font/size/icon/bg
- Add a Featured sidebar row (gold star, publisher-curated meta.featured),
  filters to featured apps; only shown when the catalog has any.
- Exact App Center font stack + 16px category text (was system-ui/14px).
- 'All' (and Featured) render as inline currentColor glyphs so the All icon
  is white like the WebUI — all.svg uses currentColor and rendered dark as an
  <img>; the fixed-blue category icons stay <img>.
- Apply --sidebar-bg to the sidebar itself too (double layer, matching the App
  Center) so the sidebar tint matches instead of showing more aurora through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-04 22:50:05 +01:00
librelad
223dc1e225 Merge claude/2 2026-07-04 22:40:07 +01:00
librelad
26e099c25f fix(marketplace/site): define the sidebar tokens so hover + separators match
--surface-hover and --sidebar-border weren't defined in the site's :root (and
--sidebar-bg had a made-up value), so the category hover/active background and
the row separators resolved to nothing — the sidebar looked flat vs the App
Center. Set them to the exact nebula values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-04 22:40:07 +01:00
librelad
fdf9dd6879 Merge claude/2 2026-07-04 22:27:57 +01:00
librelad
3be0e340ea fix(marketplace/site): keep the empty focus bar hidden (class display vs [hidden])
.focusbar's display:flex was overriding the hidden attribute, leaving a thin
empty bar under the status strip when no app was focused.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-04 22:27:57 +01:00
librelad
ace415d654 Merge claude/2 2026-07-04 22:23:28 +01:00
librelad
c0133cd437 feat(marketplace): match the App Center sidebar + simplify the app name/blurb
Website sidebar now mirrors the App Center (sidebar.css): a 220px full-height
column, full-width category rows with bottom-border separators and the real
per-category icons (bundled into the site by the install hook from the live
frontend), and the same search box — instead of the rounded pills + generic
circles + count badges it had. Marketplace app copy shortened to peer style:
description 'App Catalog & Registry', one-sentence long description.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-04 22:23:28 +01:00
librelad
8e08e85db3 Merge claude/2 2026-07-04 22:04:46 +01:00
librelad
cc36d86e57 feat(webui/apps): registry Add opens a detail modal with a marketplace link
Replaces the extra on-card marketplace tags (the separate Official trust
badge) with a richer, more elegant flow: a registry card keeps a single
'Available' pill, and clicking Add (or the card) opens a modal outlining the
app — full description, publisher + trust, version, the add command, and a
'View full page on the marketplace ↗' link to the app's page on the source
it came from — with the actual add as the confirm. Gives a beat to review a
community app before pulling it in.

- webui_registry_scan.sh re-emits source{base,channel}; loadApps stashes it
  (window.registryCatalogSource) + carries per-app version.
- openRegistryDetails() builds the eo-modal; addRegistryApp() is now just the
  task dispatch (the modal's confirm / a fallback).
- The marketplace website supports a #<slug> deep-link (focus one app, with a
  'Show all' bar), so the modal's link lands on that app's page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-04 22:04:46 +01:00
librelad
88bdb4d63a Merge claude/2 2026-07-04 21:55:54 +01:00
librelad
8e7537d717 feat(marketplace/site): replicate the App Center design on the browse website
The standalone marketplace site now mirrors the App Center it feeds: nebula
aurora background, the same app-card grid (70px icon, title, description /
category / Available / Official tags, italic long description, indigo Add
button), a left category sidebar with search, and a top bar. Client-rendered
from the same signed index.json; the Add button copies
'libreportal app add <slug>'. The status strip probes for a detached
signature rather than asserting verification (boxes verify, not the site).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-04 21:55:54 +01:00
librelad
cac431ac21 Merge claude/2 2026-07-04 21:50:57 +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
81b11e5675 Merge claude/2 2026-07-04 21:38:12 +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