203 lines
9.7 KiB
JavaScript
203 lines
9.7 KiB
JavaScript
// ==========================================
|
|
// Chat Switchboard — Admin Surfaces UI
|
|
// ==========================================
|
|
// v0.25.0: Surface lifecycle management.
|
|
// Renders in the "Surfaces" admin section via ADMIN_LOADERS.
|
|
//
|
|
// Exports: window._loadAdminSurfaces
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
async function _loadAdminSurfaces() {
|
|
const container = document.getElementById('adminSurfacesContent');
|
|
if (!container) return;
|
|
|
|
container.innerHTML = `
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
|
|
<p style="font-size:13px;color:var(--text-2);margin:0;line-height:1.6;flex:1;">
|
|
Manage which surfaces are available. Disabled surfaces redirect to Chat and are hidden from navigation.
|
|
</p>
|
|
<button id="adminSurfaceInstallBtn" class="btn-small" style="margin-left:16px;white-space:nowrap;">
|
|
+ Install Surface
|
|
</button>
|
|
</div>
|
|
<div id="adminSurfaceInstallForm" style="display:none;margin-bottom:16px;padding:16px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;">
|
|
<div style="font-size:13px;font-weight:600;margin-bottom:8px;">Install Surface Archive</div>
|
|
<p style="font-size:12px;color:var(--text-3);margin:0 0 12px;">Upload a <code>.surface</code> or <code>.zip</code> archive containing manifest.json, js/, css/, and assets/.</p>
|
|
<div style="display:flex;align-items:center;gap:8px;">
|
|
<input type="file" id="adminSurfaceFile" accept=".surface,.zip" style="font-size:12px;color:var(--text-2);">
|
|
<button id="adminSurfaceUploadBtn" class="btn-small btn-primary" disabled>Upload</button>
|
|
<button id="adminSurfaceCancelBtn" class="btn-small">Cancel</button>
|
|
</div>
|
|
<div id="adminSurfaceInstallStatus" style="font-size:12px;margin-top:8px;"></div>
|
|
</div>
|
|
<div id="adminSurfaceList" class="admin-surface-list">
|
|
<div style="padding:12px;font-size:12px;color:var(--text-3);">Loading…</div>
|
|
</div>
|
|
`;
|
|
|
|
// Wire install form
|
|
var installBtn = document.getElementById('adminSurfaceInstallBtn');
|
|
var installForm = document.getElementById('adminSurfaceInstallForm');
|
|
var fileInput = document.getElementById('adminSurfaceFile');
|
|
var uploadBtn = document.getElementById('adminSurfaceUploadBtn');
|
|
var cancelBtn = document.getElementById('adminSurfaceCancelBtn');
|
|
var statusEl = document.getElementById('adminSurfaceInstallStatus');
|
|
|
|
if (installBtn) installBtn.addEventListener('click', function() {
|
|
installForm.style.display = '';
|
|
installBtn.style.display = 'none';
|
|
});
|
|
if (cancelBtn) cancelBtn.addEventListener('click', function() {
|
|
installForm.style.display = 'none';
|
|
installBtn.style.display = '';
|
|
fileInput.value = '';
|
|
uploadBtn.disabled = true;
|
|
statusEl.textContent = '';
|
|
});
|
|
if (fileInput) fileInput.addEventListener('change', function() {
|
|
uploadBtn.disabled = !fileInput.files.length;
|
|
});
|
|
if (uploadBtn) uploadBtn.addEventListener('click', async function() {
|
|
var file = fileInput.files[0];
|
|
if (!file) return;
|
|
uploadBtn.disabled = true;
|
|
uploadBtn.textContent = 'Uploading…';
|
|
statusEl.textContent = '';
|
|
statusEl.style.color = 'var(--text-3)';
|
|
|
|
try {
|
|
var formData = new FormData();
|
|
formData.append('file', file);
|
|
var base = window.__BASE__ || '';
|
|
var resp = await fetch(base + '/api/v1/admin/surfaces/install', {
|
|
method: 'POST',
|
|
headers: API._authHeaders ? (function() { var h = API._authHeaders(); delete h['Content-Type']; return h; })() : {},
|
|
body: formData,
|
|
});
|
|
var data = await resp.json();
|
|
if (!resp.ok) throw new Error(data.error || 'Upload failed');
|
|
|
|
statusEl.style.color = 'var(--success)';
|
|
statusEl.textContent = 'Installed: ' + (data.title || data.id);
|
|
if (typeof UI !== 'undefined') UI.toast('Surface installed: ' + (data.title || data.id), 'success');
|
|
|
|
// Refresh the list
|
|
installForm.style.display = 'none';
|
|
installBtn.style.display = '';
|
|
fileInput.value = '';
|
|
_loadSurfaceList();
|
|
} catch (e) {
|
|
statusEl.style.color = 'var(--danger)';
|
|
statusEl.textContent = 'Error: ' + e.message;
|
|
uploadBtn.disabled = false;
|
|
uploadBtn.textContent = 'Upload';
|
|
}
|
|
});
|
|
|
|
_loadSurfaceList();
|
|
}
|
|
|
|
async function _loadSurfaceList() {
|
|
var listEl = document.getElementById('adminSurfaceList');
|
|
if (!listEl) return;
|
|
|
|
try {
|
|
var resp = await API._get('/api/v1/admin/surfaces');
|
|
var surfaces = resp.surfaces || [];
|
|
|
|
if (surfaces.length === 0) {
|
|
listEl.innerHTML = '<div style="padding:12px;font-size:12px;color:var(--text-3);">No surfaces registered</div>';
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = '';
|
|
surfaces.forEach(function(s) {
|
|
var row = document.createElement('div');
|
|
row.className = 'admin-surface-row';
|
|
row.dataset.surfaceId = s.id;
|
|
|
|
var isProtected = s.id === 'chat' || s.id === 'admin';
|
|
var sourceLabel = s.source === 'core' ? 'Core' : 'Extension';
|
|
var route = s.manifest && s.manifest.route ? s.manifest.route : '';
|
|
|
|
row.innerHTML =
|
|
'<div class="admin-surface-info">' +
|
|
'<div class="admin-surface-title">' +
|
|
'<span class="admin-surface-name">' + esc(s.title) + '</span>' +
|
|
'<span class="admin-surface-badge admin-surface-badge--' + s.source + '">' + sourceLabel + '</span>' +
|
|
(isProtected ? '<span class="admin-surface-badge admin-surface-badge--required">Required</span>' : '') +
|
|
'</div>' +
|
|
'<div class="admin-surface-meta">' +
|
|
'<span class="admin-surface-id">' + esc(s.id) + '</span>' +
|
|
(route ? '<span class="admin-surface-route">' + esc(route) + '</span>' : '') +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="admin-surface-actions">' +
|
|
'<label class="admin-surface-toggle' + (isProtected ? ' admin-surface-toggle--locked' : '') + '">' +
|
|
'<input type="checkbox"' + (s.enabled ? ' checked' : '') + (isProtected ? ' disabled' : '') +
|
|
' data-surface-id="' + esc(s.id) + '">' +
|
|
'<span class="admin-surface-toggle-label">' + (s.enabled ? 'Enabled' : 'Disabled') + '</span>' +
|
|
'</label>' +
|
|
(s.source === 'extension' ? '<button class="btn-small btn-danger admin-surface-uninstall" data-surface-id="' + esc(s.id) + '">Uninstall</button>' : '') +
|
|
'</div>';
|
|
|
|
// Toggle handler
|
|
var checkbox = row.querySelector('input[type="checkbox"]');
|
|
if (checkbox && !isProtected) {
|
|
checkbox.addEventListener('change', (function(surface) {
|
|
return async function() {
|
|
var id = this.dataset.surfaceId;
|
|
var action = this.checked ? 'enable' : 'disable';
|
|
var label = row.querySelector('.admin-surface-toggle-label');
|
|
var cb = this;
|
|
|
|
try {
|
|
await API._put('/api/v1/admin/surfaces/' + id + '/' + action);
|
|
if (label) label.textContent = cb.checked ? 'Enabled' : 'Disabled';
|
|
if (typeof UI !== 'undefined') {
|
|
UI.toast(surface.title + ' ' + (cb.checked ? 'enabled' : 'disabled') + ' — takes effect on next page load', 'success');
|
|
}
|
|
} catch (e) {
|
|
cb.checked = !cb.checked;
|
|
if (label) label.textContent = cb.checked ? 'Enabled' : 'Disabled';
|
|
if (typeof UI !== 'undefined') UI.toast('Failed: ' + e.message, 'error');
|
|
}
|
|
};
|
|
})(s));
|
|
}
|
|
|
|
// Uninstall handler
|
|
var uninstallBtn = row.querySelector('.admin-surface-uninstall');
|
|
if (uninstallBtn) {
|
|
uninstallBtn.addEventListener('click', (function(surface) {
|
|
return async function() {
|
|
var ok = typeof showConfirm === 'function'
|
|
? await showConfirm('Uninstall ' + surface.title + '? This removes all files.')
|
|
: window.confirm('Uninstall ' + surface.title + '?');
|
|
if (!ok) return;
|
|
|
|
try {
|
|
await API._del('/api/v1/admin/surfaces/' + surface.id);
|
|
row.remove();
|
|
if (typeof UI !== 'undefined') UI.toast(surface.title + ' uninstalled', 'success');
|
|
} catch (e) {
|
|
if (typeof UI !== 'undefined') UI.toast('Failed: ' + e.message, 'error');
|
|
}
|
|
};
|
|
})(s));
|
|
}
|
|
|
|
listEl.appendChild(row);
|
|
});
|
|
} catch (e) {
|
|
listEl.innerHTML = '<div style="padding:12px;font-size:12px;color:var(--danger);">Failed to load: ' + esc(e.message) + '</div>';
|
|
}
|
|
}
|
|
|
|
|
|
// ── Exports ─────────────────────────────────
|
|
window._loadAdminSurfaces = _loadAdminSurfaces;
|
|
})();
|