Merge claude/2
This commit is contained in:
commit
2b54be933f
@ -217,6 +217,60 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Registry catalog ("marketplace") cards — apps published in the signed index
|
||||||
|
but not on this box yet. Indigo family: distinct from green (installed),
|
||||||
|
gray (not installed) and amber (improvements). */
|
||||||
|
.app-card.registry {
|
||||||
|
border-color: rgba(99, 102, 241, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-tag.available-tag {
|
||||||
|
background: rgba(99, 102, 241, 0.30);
|
||||||
|
color: #a5b4fc;
|
||||||
|
border-color: rgba(99, 102, 241, 0.65);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-tag.available-tag::before {
|
||||||
|
content: '↓';
|
||||||
|
margin-right: 5px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Official trust badge — teal check: this entry is published and signed by
|
||||||
|
the LibrePortal team key (the box verified the signature). */
|
||||||
|
.app-tag.trust-badge {
|
||||||
|
background: rgba(34, 211, 238, 0.22);
|
||||||
|
color: #67e8f9;
|
||||||
|
border-color: rgba(34, 211, 238, 0.55);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-tag.trust-badge::before {
|
||||||
|
content: '✓';
|
||||||
|
margin-right: 5px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The Add button — indigo to match the Available pill (install stays green). */
|
||||||
|
.app-card-actions button[class*="add-btn"] {
|
||||||
|
background: #6366f1 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border: 1px solid #6366f1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card-actions button[class*="add-btn"]:hover {
|
||||||
|
background: #4f46e5 !important;
|
||||||
|
border-color: #4f46e5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card-actions button[class*="add-btn"]:disabled {
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
/* Clickable tags (category / installed-status) — jump to that filter view */
|
/* Clickable tags (category / installed-status) — jump to that filter view */
|
||||||
.app-tag.clickable {
|
.app-tag.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@ -153,6 +153,11 @@ Object.assign(AppsManager.prototype, {
|
|||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'app-card';
|
card.className = 'app-card';
|
||||||
if (app.installed) card.classList.add('installed');
|
if (app.installed) card.classList.add('installed');
|
||||||
|
// Registry catalog card ("Available — Add"): no local definition exists
|
||||||
|
// yet, so there is no detail page to open — the card is the whole surface
|
||||||
|
// until Add lands the definition and it becomes a normal Install card.
|
||||||
|
const isRegistry = !!app.registry;
|
||||||
|
if (isRegistry) card.classList.add('registry');
|
||||||
|
|
||||||
// Searchable text for the sidebar search box. Combined name +
|
// Searchable text for the sidebar search box. Combined name +
|
||||||
// description + long description + category, lowercased once here
|
// description + long description + category, lowercased once here
|
||||||
@ -227,30 +232,40 @@ 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).
|
||||||
|
const statusTag = isRegistry
|
||||||
|
? `<span class="app-tag available-tag">Available</span>${app.trust === 'official' ? `<span class="app-tag trust-badge" title="Published and signed by ${app.publisher || 'LibrePortal'}">Official</span>` : ''}`
|
||||||
|
: `<span class="app-tag clickable ${app.installed ? 'installed-tag' : 'not-installed-tag'}" onclick="event.stopPropagation(); appsManager.switchCategory('${app.installed ? 'installed' : 'all'}')">${status}</span>`;
|
||||||
|
const topAttrs = isRegistry ? '' : `style="cursor: pointer;" onclick="appsManager.showAppDetail('${appName}')"`;
|
||||||
|
const actionButton = isRegistry
|
||||||
|
? `<button class="add-btn" onclick="appsManager.addRegistryApp('${app.slug}', this)">Add</button>`
|
||||||
|
: `<button class="${app.installed ? 'manage-btn' : 'install-btn'}" onclick="appsManager.showAppDetailWithConfig('${appName}')">
|
||||||
|
${app.installed ? 'Manage' : 'Install'}
|
||||||
|
</button>`;
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="app-card-top" style="cursor: pointer;" onclick="appsManager.showAppDetail('${appName}')">
|
<div class="app-card-top" ${topAttrs}>
|
||||||
<div class="app-card-icon">
|
<div class="app-card-icon">
|
||||||
<img src="${icon}" alt="${app.name}" onerror="this.src='/core/icons/apps/default.svg'"/>
|
<img src="${icon}" alt="${app.name}" onerror="this.src='/core/icons/apps/default.svg'"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="app-card-content">
|
<div class="app-card-content">
|
||||||
<div class="app-card-title" style="cursor: pointer;">${app.name.split(' - ')[0].trim()}</div>
|
<div class="app-card-title" ${isRegistry ? '' : 'style="cursor: pointer;"'}>${app.name.split(' - ')[0].trim()}</div>
|
||||||
<div class="app-card-tags">
|
<div class="app-card-tags">
|
||||||
${descriptionTag}
|
${descriptionTag}
|
||||||
${categoryTag}
|
${categoryTag}
|
||||||
${instanceCountChip}
|
${instanceCountChip}
|
||||||
<span class="app-tag clickable ${app.installed ? 'installed-tag' : 'not-installed-tag'}" onclick="event.stopPropagation(); appsManager.switchCategory('${app.installed ? 'installed' : 'all'}')">${status}</span>
|
${statusTag}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${formattedLongDescription ? `<div class="app-card-long-description">${formattedLongDescription}</div>` : ''}
|
${formattedLongDescription ? `<div class="app-card-long-description">${formattedLongDescription}</div>` : ''}
|
||||||
<div class="app-card-actions">
|
<div class="app-card-actions">
|
||||||
<button class="${app.installed ? 'manage-btn' : 'install-btn'}" onclick="appsManager.showAppDetailWithConfig('${appName}')">
|
${actionButton}
|
||||||
${app.installed ? 'Manage' : 'Install'}
|
|
||||||
</button>
|
|
||||||
${serviceTrigger}
|
${serviceTrigger}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
},
|
},
|
||||||
// Populate inline service trigger popups for installed apps
|
// Populate inline service trigger popups for installed apps
|
||||||
|
|||||||
@ -98,6 +98,20 @@ class AppsManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Registry Add — the definition landed (or the add failed); either way the
|
||||||
|
// card's state now comes from regenerated data, so reload + repaint the
|
||||||
|
// grid: on success the Available card becomes a normal Install card, on
|
||||||
|
// failure it re-renders with its Add button re-enabled.
|
||||||
|
if (action === 'app_add') {
|
||||||
|
this.clearCache();
|
||||||
|
await this.reloadAppsData();
|
||||||
|
const p = window.location.pathname;
|
||||||
|
if (p === '/apps' || p.startsWith('/apps/')) {
|
||||||
|
this.renderApps(window.appsCategory || 'all');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// First-install welcome modal — only on the very first successful install per app per browser.
|
// First-install welcome modal — only on the very first successful install per app per browser.
|
||||||
if (action === 'install' && status === 'completed' && appName) {
|
if (action === 'install' && status === 'completed' && appName) {
|
||||||
const key = `libreportal.welcomeShown.${String(appName).toLowerCase()}`;
|
const key = `libreportal.welcomeShown.${String(appName).toLowerCase()}`;
|
||||||
@ -257,6 +271,37 @@ class AppsManager {
|
|||||||
// Filter apps by category
|
// Filter apps by category
|
||||||
let filteredApps = appsData.apps || [];
|
let filteredApps = appsData.apps || [];
|
||||||
|
|
||||||
|
// Registry catalog (marketplace): apps published in the signed index but
|
||||||
|
// not defined on this box render as "Available — Add" cards. The catalog
|
||||||
|
// is optional data — any failure means "no registry cards", never a
|
||||||
|
// broken grid. Local definitions always win on a slug collision.
|
||||||
|
try {
|
||||||
|
const regRes = await fetch('/data/apps/generated/registry_catalog.json', { cache: 'no-store' });
|
||||||
|
if (regRes.ok) {
|
||||||
|
const reg = await regRes.json();
|
||||||
|
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))
|
||||||
|
.map(e => ({
|
||||||
|
name: `${e.title || e.app} - ${e.description || e.why || ''}`,
|
||||||
|
description: e.description || e.why || '',
|
||||||
|
longDescription: e.long_description || '',
|
||||||
|
category: String(e.category || '').toLowerCase(),
|
||||||
|
categories: e.category ? [String(e.category).toLowerCase()] : [],
|
||||||
|
installed: false,
|
||||||
|
registry: true,
|
||||||
|
artifactId: e.id,
|
||||||
|
slug: e.app,
|
||||||
|
trust: e.trust || 'official',
|
||||||
|
publisher: e.publisher || '',
|
||||||
|
icon: e.icon || '/core/icons/apps/default.svg',
|
||||||
|
command: `libreportal app add ${e.app}`,
|
||||||
|
config: {}
|
||||||
|
}));
|
||||||
|
filteredApps = filteredApps.concat(registryApps);
|
||||||
|
}
|
||||||
|
} catch (_) { /* catalog unavailable — grid renders local apps only */ }
|
||||||
|
|
||||||
// Instances are managed inside their type's detail page (the family
|
// Instances are managed inside their type's detail page (the family
|
||||||
// switcher), never as standalone grid cards — hide any app that declares
|
// switcher), never as standalone grid cards — hide any app that declares
|
||||||
// INSTANCE_OF so the grid shows one card per app type.
|
// INSTANCE_OF so the grid shows one card per app type.
|
||||||
@ -277,12 +322,9 @@ class AppsManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort installed apps first
|
// Sort: installed first, then local not-installed, registry cards last.
|
||||||
filteredApps.sort((a, b) => {
|
const rank = (a) => a.installed ? 0 : (a.registry ? 2 : 1);
|
||||||
if (a.installed && !b.installed) return -1;
|
filteredApps.sort((a, b) => rank(a) - rank(b));
|
||||||
if (!a.installed && b.installed) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
this.cache.set(category, filteredApps);
|
this.cache.set(category, filteredApps);
|
||||||
@ -1146,6 +1188,18 @@ 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) {
|
||||||
|
if (!/^[a-z0-9][a-z0-9_]{0,31}$/.test(String(slug || ''))) return;
|
||||||
|
if (!window.tasksManager?.router) {
|
||||||
|
window.notifications?.error?.('Task system not ready — try again in a moment.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (btn) { btn.disabled = true; btn.textContent = 'Adding…'; }
|
||||||
|
window.tasksManager.router.routeAction('app_add', { slug });
|
||||||
|
}
|
||||||
|
|
||||||
async installApp(appName) {
|
async installApp(appName) {
|
||||||
const installedApp = (window.apps || []).find(a =>
|
const installedApp = (window.apps || []).find(a =>
|
||||||
(a.command || '').endsWith(` ${appName}`) || a.name === appName
|
(a.command || '').endsWith(` ${appName}`) || a.name === appName
|
||||||
|
|||||||
@ -141,6 +141,7 @@ Object.assign(TasksManager.prototype, {
|
|||||||
'updater_check': 'Check for Updates', 'updater_apply': 'Update',
|
'updater_check': 'Check for Updates', 'updater_apply': 'Update',
|
||||||
'updater_apply_all': 'Update All', 'updater_rollback': 'Roll Back',
|
'updater_apply_all': 'Update All', 'updater_rollback': 'Roll Back',
|
||||||
'artifact_apply': 'Apply Hotfix', 'artifact_revert': 'Revert Hotfix',
|
'artifact_apply': 'Apply Hotfix', 'artifact_revert': 'Revert Hotfix',
|
||||||
|
'app_add': 'Add App',
|
||||||
'setup-config': 'Apply Configuration',
|
'setup-config': 'Apply Configuration',
|
||||||
'setup-finalize': 'Finalize Setup'
|
'setup-finalize': 'Finalize Setup'
|
||||||
};
|
};
|
||||||
|
|||||||
@ -358,6 +358,16 @@ async configUpdate(changes) {
|
|||||||
} catch (error) { throw new Error(`Failed to revert hotfix ${id}: ${error.message}`); }
|
} catch (error) { throw new Error(`Failed to revert hotfix ${id}: ${error.message}`); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Registry catalog (marketplace) — add an app definition from the signed
|
||||||
|
// index via `libreportal app add` as a task. Slug charset-guarded before it
|
||||||
|
// enters the command string (defense in depth; the CLI re-guards).
|
||||||
|
async appAdd(slug) {
|
||||||
|
if (!slug || !/^[a-z0-9][a-z0-9_]{0,31}$/.test(slug)) throw new Error('Invalid app slug');
|
||||||
|
try {
|
||||||
|
return await this.executeTask('app_add', slug, `libreportal app add ${slug}`, `Add ${slug}`);
|
||||||
|
} catch (error) { throw new Error(`Failed to add ${slug}: ${error.message}`); }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a task object
|
* Create a task object
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -95,6 +95,11 @@ class TaskRouter {
|
|||||||
case 'artifact_revert':
|
case 'artifact_revert':
|
||||||
return await this.actions.artifactRevert(params.id);
|
return await this.actions.artifactRevert(params.id);
|
||||||
|
|
||||||
|
// Registry catalog (marketplace) — add an app definition from the
|
||||||
|
// signed index; the App Center's "Available — Add" cards route here.
|
||||||
|
case 'app_add':
|
||||||
|
return await this.actions.appAdd(params.slug);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown action: ${action}`);
|
throw new Error(`Unknown action: ${action}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user