diff --git a/containers/libreportal/frontend/components/apps/core/css/apps.css b/containers/libreportal/frontend/components/apps/core/css/apps.css
index 53b7504..394fde8 100644
--- a/containers/libreportal/frontend/components/apps/core/css/apps.css
+++ b/containers/libreportal/frontend/components/apps/core/css/apps.css
@@ -271,6 +271,14 @@
cursor: default;
}
+/* Registry "Add" detail modal bits (openRegistryDetails). */
+.mkt-detail-about { color: var(--text-secondary); line-height: 1.55; margin: 0; }
+.mkt-detail-cmd {
+ display: block; font-size: 0.82rem; padding: 8px 10px; border-radius: 8px;
+ background: rgba(0, 0, 0, 0.3); border: 1px solid var(--border-color); overflow-x: auto; white-space: nowrap;
+}
+.mkt-detail-link { font-weight: 600; }
+
/* Clickable tags (category / installed-status) — jump to that filter view */
.app-tag.clickable {
cursor: pointer;
diff --git a/containers/libreportal/frontend/components/apps/core/js/apps-grid.js b/containers/libreportal/frontend/components/apps/core/js/apps-grid.js
index 3976b83..4502f78 100644
--- a/containers/libreportal/frontend/components/apps/core/js/apps-grid.js
+++ b/containers/libreportal/frontend/components/apps/core/js/apps-grid.js
@@ -232,14 +232,17 @@ Object.assign(AppsManager.prototype, {
}
}
- // Registry cards: an "Available" pill + official trust badge instead of
- // the install-state pill; the top area is not clickable (no detail page).
+ // Registry cards: a single "Available" status pill (the richer detail —
+ // publisher, trust, version, the marketplace link — lives in the Add modal,
+ // so the card stays clean). Clicking the card top OR Add opens that modal.
const statusTag = isRegistry
- ? `Available${app.trust === 'official' ? `Official` : ''}`
+ ? `Available`
: `${status}`;
- const topAttrs = isRegistry ? '' : `style="cursor: pointer;" onclick="appsManager.showAppDetail('${appName}')"`;
+ const topAttrs = isRegistry
+ ? `style="cursor: pointer;" onclick="appsManager.openRegistryDetails('${app.slug}')"`
+ : `style="cursor: pointer;" onclick="appsManager.showAppDetail('${appName}')"`;
const actionButton = isRegistry
- ? ``
+ ? ``
: ``;
@@ -250,7 +253,7 @@ Object.assign(AppsManager.prototype, {
-
${app.name.split(' - ')[0].trim()}
+
${app.name.split(' - ')[0].trim()}
${descriptionTag}
${categoryTag}
diff --git a/containers/libreportal/frontend/components/apps/core/js/apps-manager.js b/containers/libreportal/frontend/components/apps/core/js/apps-manager.js
index 8e9809b..5dbe6ab 100755
--- a/containers/libreportal/frontend/components/apps/core/js/apps-manager.js
+++ b/containers/libreportal/frontend/components/apps/core/js/apps-manager.js
@@ -279,6 +279,9 @@ class AppsManager {
const regRes = await fetch('/data/apps/generated/registry_catalog.json', { cache: 'no-store' });
if (regRes.ok) {
const reg = await regRes.json();
+ // Stash the catalog source (base URL + channel) so the Add modal can
+ // link to the app's full page on the marketplace it came from.
+ window.registryCatalogSource = reg.source || null;
const localSlugs = new Set(filteredApps.map(a => (a.command || '').split(' ').pop()));
const registryApps = (reg.apps || [])
.filter(e => e && e.app && !e.defined && !e.installed && !localSlugs.has(e.app))
@@ -292,6 +295,7 @@ class AppsManager {
registry: true,
artifactId: e.id,
slug: e.app,
+ version: e.version || 1,
trust: e.trust || 'official',
publisher: e.publisher || '',
icon: e.icon || '/core/icons/apps/default.svg',
@@ -1201,18 +1205,73 @@ class AppsManager {
- // Registry catalog "Add" — dispatch the app_add task that verifies the
- // signed bundle and drops the definition in (mutations via tasks, as ever).
- addRegistryApp(slug, btn = null) {
+ // Registry catalog "Add" — the elegant path: open a detail modal (full
+ // description, publisher, trust, version, a link to the app's page on the
+ // marketplace it came from) with the actual add as the confirm. Keeps the
+ // grid card clean and gives a beat to review a community app before pulling
+ // it in. The card carries the registry fields (loadApps mapping).
+ openRegistryDetails(slug) {
+ if (!/^[a-z0-9][a-z0-9_]{0,31}$/.test(String(slug || ''))) return;
+ const a = (window.apps || []).find(x => x.registry && x.slug === slug);
+ if (!a) { this.addRegistryApp(slug); return; } // data missing — fall back to a plain add
+ if (typeof window.openEoModal !== 'function') { this.addRegistryApp(slug); return; }
+
+ const official = a.trust === 'official';
+ const badges = (typeof window.eoBadgeRow === 'function') ? window.eoBadgeRow([
+ { icon: '🏷️', label: a.category || 'app', variant: 'info' },
+ { icon: official ? '✓' : '•', label: official ? `Official — signed by ${a.publisher || 'LibrePortal'}` : `Community — ${a.publisher || 'unverified'}`, variant: official ? 'success' : 'warning' },
+ { icon: '⑆', label: `v${a.version || 1}`, variant: 'purple' },
+ ]) : '';
+ const sec = (typeof window.eoSection === 'function') ? window.eoSection : (t, c) => `
${t}${c}
`;
+ const longDesc = a.longDescription || a.description || 'No description provided.';
+
+ // Link to the app's full page on the marketplace it came from, when the
+ // catalog told us its source base (host that serves the browse site).
+ const src = window.registryCatalogSource;
+ let linkHtml = '';
+ if (src && src.base && /^https?:\/\//.test(src.base)) {
+ const url = `${src.base.replace(/\/+$/, '')}/#${encodeURIComponent(slug)}`;
+ linkHtml = sec('Marketplace', `View full page on the marketplace ↗`);
+ }
+
+ const body = [
+ badges,
+ sec('About', `