Changeset 0.30.0 (#199)

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit is contained in:
2026-03-18 12:06:55 +00:00
committed by xcaliber
parent 7f191e18cd
commit 7d14e6a439
15 changed files with 1625 additions and 77 deletions

View File

@@ -3,6 +3,7 @@
// ==========================================
// Renders in the "Packages" admin section via ADMIN_LOADERS.
// Replaces admin-surfaces.js (v0.28.7).
// v0.30.0: scope column, settings, export, registry browse.
//
// Exports: window._loadAdminPackages
@@ -20,6 +21,8 @@ async function _loadAdminPackages() {
'<button class="btn-small" id="pkgFilterSurface">Surfaces</button>' +
'<button class="btn-small" id="pkgFilterExtension">Extensions</button>' +
'<button class="btn-small" id="pkgFilterFull">Full</button>' +
'<span style="flex:1"></span>' +
'<button class="btn-small" id="pkgBrowseRegistry">Browse Registry</button>' +
'</div>' +
'</div>' +
'<div id="adminPkgInstallForm" style="display:none;margin-bottom:16px;padding:14px;background:var(--surface);border:1px solid var(--border);border-radius:8px">' +
@@ -30,6 +33,8 @@ async function _loadAdminPackages() {
'<button class="btn-small" id="pkgInstallCancel">Cancel</button>' +
'</div>' +
'</div>' +
'<div id="adminPkgSettingsPanel" style="display:none;margin-bottom:16px;padding:14px;background:var(--surface);border:1px solid var(--border);border-radius:8px"></div>' +
'<div id="adminPkgRegistryPanel" style="display:none;margin-bottom:16px;padding:14px;background:var(--surface);border:1px solid var(--border);border-radius:8px"></div>' +
'<div id="adminPkgList" class="admin-list"><div class="loading">Loading...</div></div>';
var base = document.body.dataset.basePath || '';
@@ -41,11 +46,18 @@ async function _loadAdminPackages() {
if (btn) {
btn.addEventListener('click', function() {
currentFilter = label === 'All' ? '' : label.toLowerCase();
document.getElementById('adminPkgRegistryPanel').style.display = 'none';
loadList();
});
}
});
// Browse Registry button
var browseBtn = document.getElementById('pkgBrowseRegistry');
if (browseBtn) {
browseBtn.addEventListener('click', loadRegistry);
}
// Install form
var installSubmit = document.getElementById('pkgInstallSubmit');
var installCancel = document.getElementById('pkgInstallCancel');
@@ -102,12 +114,13 @@ async function _loadAdminPackages() {
}
var html = '<table class="admin-table" style="width:100%"><thead><tr>' +
'<th style="width:30%">Package</th>' +
'<th style="width:25%">Package</th>' +
'<th>Type</th>' +
'<th>Version</th>' +
'<th>Scope</th>' +
'<th>Source</th>' +
'<th>Status</th>' +
'<th style="width:100px">Actions</th>' +
'<th style="width:140px">Actions</th>' +
'</tr></thead><tbody>';
packages.forEach(function(pkg) {
@@ -121,8 +134,15 @@ async function _loadAdminPackages() {
core: '<span class="text-muted">core</span>',
builtin: '<span class="text-muted">builtin</span>',
extension: '<span class="text-muted">installed</span>',
registry: '<span class="text-muted">registry</span>',
}[pkg.source] || pkg.source;
var scopeBadge = {
global: '<span class="text-muted">global</span>',
team: '<span class="badge badge-info">team</span>',
personal: '<span class="badge badge-accent">personal</span>',
}[pkg.scope] || pkg.scope;
var statusBadge = pkg.enabled
? '<span class="badge badge-success">enabled</span>'
: '<span class="badge badge-muted">disabled</span>';
@@ -136,6 +156,14 @@ async function _loadAdminPackages() {
actions += '<button class="btn-small btn-ghost pkg-enable" data-id="' + pkg.id + '" title="Enable">Enable</button> ';
}
}
// Settings (if manifest has settings)
if (pkg.manifest && pkg.manifest.settings && pkg.manifest.settings.length > 0) {
actions += '<button class="btn-small btn-ghost pkg-settings" data-id="' + pkg.id + '" title="Settings">Settings</button> ';
}
// Export (non-core only)
if (pkg.source !== 'core') {
actions += '<button class="btn-small btn-ghost pkg-export" data-id="' + pkg.id + '" title="Export">Export</button> ';
}
// Delete (non-core only)
if (pkg.source !== 'core') {
actions += '<button class="btn-small btn-danger-ghost pkg-delete" data-id="' + pkg.id + '" title="Delete">Delete</button>';
@@ -143,11 +171,13 @@ async function _loadAdminPackages() {
var desc = pkg.description ? '<div class="text-muted" style="font-size:11px">' + pkg.description + '</div>' : '';
var system = pkg.is_system ? ' <span class="badge badge-muted" style="font-size:10px">system</span>' : '';
var schemaVer = pkg.schema_version > 0 ? ' <span class="text-muted" style="font-size:10px">v' + pkg.schema_version + '</span>' : '';
html += '<tr>' +
'<td><strong>' + pkg.title + '</strong>' + system + '<div class="text-muted" style="font-size:11px">' + pkg.id + '</div>' + desc + '</td>' +
'<td><strong>' + pkg.title + '</strong>' + system + schemaVer + '<div class="text-muted" style="font-size:11px">' + pkg.id + '</div>' + desc + '</td>' +
'<td>' + typeBadge + '</td>' +
'<td style="font-size:12px;font-family:monospace">' + (pkg.version || '') + '</td>' +
'<td style="font-size:12px;font-family:monospace">' + (pkg.version || '\u2014') + '</td>' +
'<td>' + scopeBadge + '</td>' +
'<td>' + sourceBadge + '</td>' +
'<td>' + statusBadge + '</td>' +
'<td>' + actions + '</td>' +
@@ -180,11 +210,170 @@ async function _loadAdminPackages() {
loadList();
});
});
listEl.querySelectorAll('.pkg-export').forEach(function(btn) {
btn.addEventListener('click', function() {
window.location = base + '/api/v1/admin/packages/' + btn.dataset.id + '/export';
});
});
listEl.querySelectorAll('.pkg-settings').forEach(function(btn) {
btn.addEventListener('click', function() {
loadSettings(btn.dataset.id);
});
});
} catch (e) {
listEl.innerHTML = '<div style="padding:12px;color:var(--danger);">Failed to load packages: ' + e.message + '</div>';
}
}
// ── Settings panel ──────────────────────────
async function loadSettings(pkgId) {
var panel = document.getElementById('adminPkgSettingsPanel');
panel.style.display = 'block';
panel.innerHTML = '<div class="loading">Loading settings...</div>';
try {
var resp = await API._get('/api/v1/admin/packages/' + pkgId + '/settings');
var schema = resp.schema || [];
var values = resp.values || {};
if (typeof values === 'string') {
try { values = JSON.parse(values); } catch(e) { values = {}; }
}
if (schema.length === 0) {
panel.innerHTML = '<p class="text-muted" style="font-size:12px">No settings declared by this package.</p>' +
'<button class="btn-small" id="pkgSettingsClose">Close</button>';
document.getElementById('pkgSettingsClose').addEventListener('click', function() { panel.style.display = 'none'; });
return;
}
var html = '<h4 style="margin:0 0 12px 0;font-size:13px">Settings: ' + pkgId + '</h4>';
schema.forEach(function(s) {
var key = s.key;
var val = values[key] !== undefined ? values[key] : (s.default !== undefined ? s.default : '');
html += '<div style="margin-bottom:10px">' +
'<label style="font-size:12px;display:block;margin-bottom:4px">' + (s.label || key) + '</label>';
if (s.type === 'select' && s.options) {
html += '<select class="pkg-setting-input" data-key="' + key + '" style="font-size:12px;padding:4px 8px">';
s.options.forEach(function(opt) {
html += '<option value="' + opt + '"' + (opt === val ? ' selected' : '') + '>' + opt + '</option>';
});
html += '</select>';
} else if (s.type === 'boolean') {
html += '<input type="checkbox" class="pkg-setting-input" data-key="' + key + '" data-type="boolean"' + (val ? ' checked' : '') + '>';
} else if (s.type === 'number') {
html += '<input type="number" class="pkg-setting-input" data-key="' + key + '" data-type="number" value="' + val + '" style="font-size:12px;padding:4px 8px;width:100px">';
} else {
html += '<input type="text" class="pkg-setting-input" data-key="' + key + '" value="' + val + '" style="font-size:12px;padding:4px 8px;width:200px">';
}
html += '</div>';
});
html += '<div style="display:flex;gap:8px;margin-top:12px">' +
'<button class="btn-small btn-primary" id="pkgSettingsSave">Save</button>' +
'<button class="btn-small" id="pkgSettingsClose">Close</button>' +
'</div>';
panel.innerHTML = html;
document.getElementById('pkgSettingsSave').addEventListener('click', async function() {
var data = {};
panel.querySelectorAll('.pkg-setting-input').forEach(function(el) {
var key = el.dataset.key;
if (el.dataset.type === 'boolean') {
data[key] = el.checked;
} else if (el.dataset.type === 'number') {
data[key] = parseFloat(el.value) || 0;
} else {
data[key] = el.value;
}
});
try {
await API._put('/api/v1/admin/packages/' + pkgId + '/settings', data);
UI.toast('Settings saved', 'success');
panel.style.display = 'none';
} catch (e) {
UI.toast('Failed to save: ' + e.message, 'error');
}
});
document.getElementById('pkgSettingsClose').addEventListener('click', function() { panel.style.display = 'none'; });
} catch (e) {
panel.innerHTML = '<div style="color:var(--danger)">Failed to load settings: ' + e.message + '</div>';
}
}
// ── Registry browse ─────────────────────────
async function loadRegistry() {
var panel = document.getElementById('adminPkgRegistryPanel');
panel.style.display = 'block';
panel.innerHTML = '<div class="loading">Loading registry...</div>';
try {
var resp = await API._get('/api/v1/admin/packages/registry');
var packages = resp.packages || [];
var registryUrl = resp.registry_url || '';
var html = '<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">' +
'<h4 style="margin:0;font-size:13px">Package Registry</h4>' +
'<span style="flex:1"></span>' +
'<button class="btn-small" id="pkgRegistryClose">Close</button>' +
'</div>';
if (!registryUrl) {
html += '<p class="text-muted" style="font-size:12px">No registry URL configured. Set <code>package_registry</code> in Admin Settings with a <code>{"url": "https://..."}</code> value.</p>';
} else if (packages.length === 0) {
html += '<p class="text-muted" style="font-size:12px">Registry is empty or returned no packages.</p>';
} else {
html += '<table class="admin-table" style="width:100%"><thead><tr>' +
'<th style="width:30%">Package</th><th>Version</th><th>Author</th><th>Actions</th>' +
'</tr></thead><tbody>';
packages.forEach(function(pkg) {
var installBtn = pkg.installed
? '<span class="badge badge-success">Installed</span>'
: '<button class="btn-small btn-primary registry-install" data-url="' + pkg.download_url + '">Install</button>';
html += '<tr>' +
'<td><strong>' + pkg.title + '</strong><div class="text-muted" style="font-size:11px">' +
(pkg.description || '') + '</div></td>' +
'<td style="font-family:monospace;font-size:12px">' + (pkg.version || '') + '</td>' +
'<td style="font-size:12px">' + (pkg.author || '') + '</td>' +
'<td>' + installBtn + '</td>' +
'</tr>';
});
html += '</tbody></table>';
}
panel.innerHTML = html;
document.getElementById('pkgRegistryClose').addEventListener('click', function() { panel.style.display = 'none'; });
panel.querySelectorAll('.registry-install').forEach(function(btn) {
btn.addEventListener('click', async function() {
btn.disabled = true;
btn.textContent = 'Installing...';
try {
await API._post('/api/v1/admin/packages/registry/install', {
download_url: btn.dataset.url,
});
UI.toast('Installed from registry', 'success');
loadList();
loadRegistry(); // refresh to show "Installed" badge
} catch (e) {
UI.toast('Install failed: ' + e.message, 'error');
btn.disabled = false;
btn.textContent = 'Install';
}
});
});
} catch (e) {
panel.innerHTML = '<div style="color:var(--danger)">Failed to load registry: ' + e.message + '</div>';
}
}
loadList();
}