1145 Commits

Author SHA1 Message Date
librelad
9830382a4a Merge claude/2 2026-07-03 21:46:29 +01:00
librelad
7fd4a1c29e fix(marketplace/site): hide the light scrollbar strip on the add-command snippets
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-03 21:46:29 +01:00
librelad
0c8b13d2f1 Merge claude/2 2026-07-03 21:43:59 +01:00
librelad
c44e17967f fix(marketplace): converge the nginx.conf bind-mount source in the install hook
The generic installer copies only config+compose into the live dir, so the
compose's nginx.conf file mount had no source — and a premature container
start leaves a directory placeholder at the mount path. The post_setup hook
now removes the placeholder and copies the file from the definition, so
(re)install converges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-03 21:43:59 +01:00
librelad
58541b1fe3 Merge claude/2 2026-07-03 21:40:33 +01:00
librelad
897f514735 feat(marketplace): the marketplace server ships as a dev-mode LibrePortal app
containers/marketplace — nginx:alpine app (standard drop-in contract:
config + tagged compose + icon + install hook) whose docroot serves BOTH
halves of the marketplace: the signed catalog channel tree (index.json /
payloads, published into data/<channel>/ by the release tools) and a
self-contained client-rendered browse site over the same file (search,
category chips, trust badges, copyable 'libreportal app add <slug>' —
no third-party assets, no backend, no build step). The official
marketplace is an instance of this app; self-hosting one = installing it
and pointing CFG_RELEASE_BASE_URL at it. Boxes only ever trust the
minisign signature on the catalog, never the website.

New generic gating convention: CFG_<APP>_DEV_ONLY=true keeps an app out
of the App Center grid unless Developer Mode is on (CFG_DEV_MODE, the
same flag the **DEV** config-field filter uses); an installed dev-only
app always stays visible. The marketplace app is the first user.

Cache policy: catalog/channel manifests no-cache; payloads short
revalidating cache (same-id re-publish); version-pinned release
artifacts immutable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-03 21:40:33 +01:00
librelad
9b5a4d276d Merge claude/2 2026-07-03 21:35:30 +01:00
librelad
52be5968e9 docs: registry slice shipped — §8.4b bundle spec addendum + §8.7 status + publish guide
Roadmap: Phase 6 entry (app bundles + Browse-&-Add, built 2026-07-03),
the app-bundle format addendum (meta object, tarball contract, collision
policy, trust-then-quarantine ordering), deferred bullet trimmed to what
actually remains (community quarantine, taps, theme/component, canary
countersign) with a pointer to marketplace-website.md for the website
layer. development.md gains the 'Publish an app to the marketplace
catalog' section (make_app.sh flow + the app add loop).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-03 21:35:30 +01:00
librelad
2b54be933f Merge claude/2 2026-07-03 21:23:14 +01:00
librelad
1165ec1799 feat(webui/apps): marketplace cards — registry apps merge into the App Center grid
The grid now also reads apps/generated/registry_catalog.json (optional
data: any failure = no registry cards, never a broken grid) and renders
signed-catalog apps that have no local definition as "Available — Add"
cards: indigo Available pill, teal Official trust badge, and an Add button
that dispatches the app_add task (task-actions/router/format wired). Local
definitions always win on a slug collision; registry cards sort last and
are excluded from the Installed view. No detail page until Add lands the
definition — then the card becomes a normal Install card, repainted by the
app_add branch in the task-completion handler without a manual refresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-03 21:23:14 +01:00
librelad
3675b6b34c Merge claude/2 2026-07-03 21:17:26 +01:00
librelad
87edd09994 feat(webui/registry): catalog scan generator + hotfix-only Improvements stream
webuiRegistryCatalogScan (run by updater check, same atomic keep-prior
pattern as webuiArtifactScan) writes apps/generated/registry_catalog.json:
the type:"app"/kind:"bundle" rows of the signed index annotated with
defined/installed, browse metadata from the envelope meta, and icons
mirrored into core/icons/apps/registry/ ONLY when their bytes match the
sha256 pin in the signed index — the browser stays same-origin; a tampered
or oversized icon is skipped, never served.

webuiArtifactScan now selects type=="hotfix" so app rows never render as
pseudo-hotfixes in the Improvements tab, and counts+logs artifacts of
unrecognized type instead of surfacing them (the §8.1 forward-compat
firewall on the scan path).

Harness vs a locally served registry: 14/14 (catalog row + meta + flags,
icon pin verify + tamper skip, hotfix-only stream, unknown-type skip+log,
unreachable-registry keeps prior files).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-03 21:17:26 +01:00
librelad
d1958f6033 Merge claude/2 2026-07-03 21:14:14 +01:00
librelad
4a1aa43083 feat(artifact): app bundle applier + libreportal app add — the marketplace verb
Opens the two designed seams (roadmap §8.4): _artifactResolve accepts
type:"app" (slug validated, the installed-app gate skipped — presence is
the collision policy's call), and payload.kind:"bundle" gets its own APPLY
flow. The download core (sha256 pin vs the signed index + minisig +
refuse-unsigned) is factored into _artifactDownloadVerified, shared by ops
and bundle payloads.

A bundle add: fetch → quarantine-validate → place in the definition tree
(staging + one rename, manager funnel) → lpRegenWebui → verify the app
surfaced in apps.json → applied-record with a precise undo → History.
The validator is fail-closed (traversal/absolute paths, links/devices,
single top-level dir == slug, charset, size/entry caps, set-id strip,
config TITLE+CATEGORY + compose present, bash -n every .sh) because the
definition tree is live-sourced on every CLI start — nothing lands there
before trust + quarantine pass. Collision policy: installed-live refused,
local definitions win, registry-owned re-add = reversible definition
update (prior tree packed into the undo). Revert removes/restores the
definition (refused while installed) and regens. Apps never auto-apply
(type filter kept + publisher forces auto:false).

New verb: libreportal app add <slug|artifact-id> (app_add task; resolves
by slug via appAddFromRegistry, ambiguity refused).

Also fixes the second half of the sigstate-propagation bug class:
artifactApply captured $(_artifactResolve) in a subshell, stranding
_ART_INDEX/_ART_APP/_ART_SCOPE AND the LP_INDEX_SIGSTATE the apply gate
enforces — on a signed box every apply would have refused as unsigned.
Resolve now assigns globals (_ART_JSON) and is called directly.

Source-and-mock harness: 46/46 (resolve gates, 14 validator refusals,
happy add, collision matrix, definition-update round-trip, revert
semantics, postcheck + record-failure rollbacks, apply-auto exclusion,
app add verb).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-03 21:14:14 +01:00
librelad
e04fdbf64f Merge claude/2 2026-07-03 20:45:02 +01:00
librelad
8351863719 feat(release): make_app.sh — publish app definition bundles to the artifact index
The marketplace publisher tool: packs containers/<slug>/ (the drop-in app
contract, unchanged) into a deterministic tarball (commit-clamped mtimes +
gzip -n; unchanged apps repack byte-identical and keep their published
version), signs it, copies a sha256-pinned catalog icon, and upserts a
type:"app" / payload.kind:"bundle" envelope into the same team-signed
index hotfixes ride. Catalog metadata (title/category/descriptions) is
parsed line-wise from the app's own .config — one source of truth with the
App Center generators. auto:false is hard-forced: apps never auto-apply.

The index-upsert/serial/freshness/publishers-map and signing logic is
factored out of make_hotfix.sh into lib/release_index.sh, shared by both
tools (make_hotfix.sh behavior preserved; regression-tested alongside an
app entry: serial bump, both payload kinds coexist, valid_until refreshed).
LP_INDEX_VALID_DAYS is the shared freshness knob (LP_HOTFIX_VALID_DAYS
kept as a legacy alias).

Verified: speedtest publish → deterministic repack (identical sha256) →
served via local http.server → real lpFetchIndex/accessors harness 10/10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-03 20:45:02 +01:00
librelad
fd5facc7cf Merge claude/2 2026-07-03 20:43:59 +01:00
librelad
36a5c87397 fix(artifacts): propagate LP_INDEX_SIGSTATE to callers via lpFetchIndexInto
Every caller captured the index with var=$(lpFetchIndex), which runs the
fetch in a command-substitution subshell — the LP_INDEX_SIGSTATE global it
sets never reached the caller. On a box with real signing active the
artifactApply/apply-auto gates would therefore refuse a correctly signed
index (fail-closed, but the apply path would be dead on arrival the day
signing activates), and artifact index / the WebUI scan would report a
verified feed as UNSIGNED.

New lpFetchIndexInto <var> [cache] runs the fetch in the calling shell and
assigns via printf -v; all four call sites converted. Verified with a
source-and-mock harness against a locally served index: 10/10 (sigstate
reaches caller, serial high-water, anti-rollback refuse, staleness refuse,
id enumeration, envelope round-trip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-03 20:43:59 +01:00
librelad
b0a194dd48 Merge claude/2 2026-07-03 20:30:07 +01:00
librelad
0afd1de819 docs(roadmap): marketplace app design — dev-mode container + catalog + submissions
The marketplace system ships as containers/libreportal-marketplace (a normal
app definition, hidden behind CFG_DEV_MODE via a new CFG_<APP>_DEV_ONLY
convention): it serves the signed catalog tree plus a client-rendered browse
UI over the same index.json every box verifies. The official instance is our
own install of this app; self-hosting a marketplace = installing it.
Community submissions stay PR -> review -> sign (phase 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-03 20:30:07 +01:00
librelad
22d23cb359 Merge claude/1 2026-07-03 19:51:26 +01:00
librelad
4abafd4640 tweak(webui/system): status dot before app icon in Per-app rows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-03 19:51:26 +01:00
librelad
0b2ed1cf1a Merge claude/1 2026-07-03 19:44:40 +01:00
librelad
de0025d1a7 feat(webui/system): app icons in Per-app usage rows
Prepend a boxed app icon (tasks-notification style) to each row of the
System page's Per-app usage table. Multi-instance slugs (<type>_<id>)
resolve to the base type's icon; missing icons fall back to default.svg.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-03 19:44:40 +01:00
librelad
61334d1e72 Merge claude/1 2026-06-25 22:06:03 +01:00
librelad
ab0822c46b feat(webui/loading): shared boxed spinner loader for page/panel loading states
Pages and panels showed inconsistent loading states: the Backup center
and several admin pages (System, SSH, admin Overview), the Overview
Migrate/Peers panels, the per-app updater section and the backup engine
details modal rendered a bare 'Loading…' text line (updater-empty /
backup-empty-state) with no spinner, while Services/Config/Tasks used a
boxed card + spinner.

Add one shared loader — window.lpLoadingBox(message) + .lp-loading CSS in
the core/loading subsystem (the boxed card + accent spinner the good tabs
already use) — and route those bare-text loaders through it. The system
metric graph keeps its absolute overlay but gains the same spinner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 22:06:03 +01:00
librelad
9e1adef2b8 Merge claude/2 2026-06-25 21:25:38 +01:00
librelad
cc026d4d68 fix(webui/updater): correct misleading improvements empty-state copy
'Checking for improvements… this usually takes a minute.' implied an active
scan finishing imminently. The scan is periodic and runs in the background
(CFG_UPDATER_SCAN_INTERVAL, default 30m), so reword to reflect that. Kept the
cadence generic since the interval is admin-configurable and not exposed here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 21:25:38 +01:00
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