Feat v0.7.0 shell contract + surface audit + rebrand
Two-slot shell topbar (home, left, center, bell, user menu) with SDK API (setLeft/setSlot/setTitle/hide/show). All 4 primary surfaces migrated: Settings and Team Admin to Pattern B (flat tabs), Admin to Pattern C (category tabs + sidebar), Docs to Pattern A (default). Backend WS events: package.changed (broadcast), auth.changed (targeted), notification.all_read. User menu re-fetches on package/auth changes. Bell syncs read state across tabs. Error handling pass with .sw-inline-error CSS primitive. Empty state guidance for Admin Workflows/Groups. Announcement global dismiss via localStorage. Rebrand assets deployed (both b/e icon variants, wordmarks, full icon library). Docs outline scroll-to-heading fix. Bug fixes: ICD security assertion tightened, workflow-demo error surfacing, signoff display names, hello-dashboard + team-admin/groups.js deleted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
/* Hello Dashboard — sample extension surface styles.
|
||||
Uses CSS custom properties from the platform theme system (variables.css).
|
||||
See EXTENSION-SURFACES.md for the full property reference. */
|
||||
|
||||
.ext-hello-dashboard { max-width: 720px; margin: 0 auto; padding: var(--sp-10) var(--sp-6); }
|
||||
.ext-hello-dashboard-header { margin-bottom: var(--sp-8); }
|
||||
.ext-hello-dashboard-header h1 { font-size: 28px; font-weight: 700; color: var(--text); margin: 0 0 var(--sp-2) 0; }
|
||||
.ext-hello-dashboard-subtitle { font-size: 14px; color: var(--text-2); margin: 0; }
|
||||
.ext-hello-dashboard-subtitle code,
|
||||
.ext-hello-dashboard-card-value code { background: var(--bg-raised); padding: 2px 6px; border-radius: var(--radius-sm); font-size: 13px; }
|
||||
.ext-hello-dashboard-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--sp-4); margin-bottom: var(--sp-6); }
|
||||
.ext-hello-dashboard-card { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--sp-4); }
|
||||
.ext-hello-dashboard-card-title { font-size: 12px; font-weight: 600; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: var(--sp-2); }
|
||||
.ext-hello-dashboard-card-value { font-size: 20px; font-weight: 600; color: var(--text); margin-bottom: var(--sp-1); }
|
||||
.ext-hello-dashboard-card-detail { font-size: 12px; color: var(--text-3); }
|
||||
.ext-hello-dashboard-actions { display: flex; gap: var(--sp-3); margin-bottom: var(--sp-6); }
|
||||
.ext-hello-dashboard-manifest { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--sp-4); font-size: 12px; color: var(--text-2); overflow-x: auto; white-space: pre-wrap; font-family: var(--mono); line-height: 1.5; }
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* Hello Dashboard — sample extension surface.
|
||||
*
|
||||
* Platform contract:
|
||||
* - Mounts into #extension-mount
|
||||
* - window.__MANIFEST__ — surface manifest (json, lowercase keys)
|
||||
* - sw.auth.user — authenticated user (from SDK boot)
|
||||
* - UI.toast(msg, type) — platform toast (success/error/info/warning)
|
||||
* - API._get(path) — authenticated fetch (returns parsed JSON)
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var mount = document.getElementById('extension-mount');
|
||||
if (!mount) return;
|
||||
|
||||
var manifest = window.__MANIFEST__ || {};
|
||||
var user = window.sw?.auth?.user || {};
|
||||
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
var name = user.display_name || user.username || 'World';
|
||||
|
||||
mount.innerHTML =
|
||||
'<div class="ext-hello-dashboard">' +
|
||||
'<div class="ext-hello-dashboard-header">' +
|
||||
'<h1>Hello, ' + esc(name) + '!</h1>' +
|
||||
'<p class="ext-hello-dashboard-subtitle">Extension surface <code>' + esc(manifest.id || 'unknown') + '</code> loaded successfully.</p>' +
|
||||
'</div>' +
|
||||
'<div class="ext-hello-dashboard-cards">' +
|
||||
card('Platform Access', (typeof UI !== 'undefined' ? '\u2713 Connected' : '\u2717 Unavailable'),
|
||||
'API, Theme, UI primitives available', 'var(--accent)') +
|
||||
card('Theme', isDark ? '\uD83C\uDF19 Dark' : '\u2600\uFE0F Light',
|
||||
'Reads from platform theme system', '') +
|
||||
card('Route', '<code>' + esc(manifest.route || '/s/hello-dashboard') + '</code>',
|
||||
'Registered via surface manifest', '') +
|
||||
'</div>' +
|
||||
'<div class="ext-hello-dashboard-actions">' +
|
||||
'<button class="sw-btn sw-btn--primary sw-btn--md" id="helloToast">Show Toast</button>' +
|
||||
'<button class="sw-btn sw-btn--secondary sw-btn--md" id="helloApi">Test API</button>' +
|
||||
'</div>' +
|
||||
'<pre class="ext-hello-dashboard-manifest">' + esc(JSON.stringify(manifest, null, 2)) + '</pre>' +
|
||||
'</div>';
|
||||
|
||||
// Wire toast button
|
||||
document.getElementById('helloToast').addEventListener('click', function() {
|
||||
if (typeof UI !== 'undefined' && UI.toast) {
|
||||
UI.toast('Extension surface is working!', 'success');
|
||||
} else {
|
||||
alert('Extension surface is working! (UI.toast not available)');
|
||||
}
|
||||
});
|
||||
|
||||
// Wire API test button
|
||||
document.getElementById('helloApi').addEventListener('click', function() {
|
||||
if (typeof API === 'undefined' || !API._get) {
|
||||
toast('API module not available', 'error');
|
||||
return;
|
||||
}
|
||||
API._get('/api/v1/surfaces').then(function(resp) {
|
||||
toast('API returned ' + (resp.surfaces || []).length + ' registered surfaces', 'info');
|
||||
}).catch(function(e) {
|
||||
toast('API error: ' + e.message, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
function toast(msg, type) {
|
||||
if (typeof UI !== 'undefined' && UI.toast) UI.toast(msg, type);
|
||||
else alert(msg);
|
||||
}
|
||||
|
||||
function card(title, value, detail, color) {
|
||||
var style = color ? ' style="color:' + color + ';"' : '';
|
||||
return '<div class="ext-hello-dashboard-card">' +
|
||||
'<div class="ext-hello-dashboard-card-title">' + esc(title) + '</div>' +
|
||||
'<div class="ext-hello-dashboard-card-value"' + style + '>' + value + '</div>' +
|
||||
'<div class="ext-hello-dashboard-card-detail">' + esc(detail) + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
var el = document.createElement('span');
|
||||
el.textContent = s;
|
||||
return el.innerHTML;
|
||||
}
|
||||
})();
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"id": "hello-dashboard",
|
||||
"icon": "👋",
|
||||
"type": "surface",
|
||||
"title": "Hello Dashboard",
|
||||
"route": "/s/hello-dashboard",
|
||||
"auth": "authenticated",
|
||||
"layout": "single",
|
||||
"components": [],
|
||||
"hooks": ["surface"],
|
||||
"version": "0.1.0",
|
||||
"description": "Sample extension surface — verifies the /s/:slug pipeline works end-to-end."
|
||||
}
|
||||
@@ -542,16 +542,22 @@
|
||||
|
||||
await T.test('security', 'input-validation', '[P0] path traversal in surface archive', async function () {
|
||||
var blob = new Blob([JSON.stringify({ id: '../../../etc/evil', title: 'Path Traversal Test' })], { type: 'application/json' });
|
||||
var d;
|
||||
try {
|
||||
var d = await T.apiUpload('/admin/packages/install', blob, 'evil.surface');
|
||||
T.assert(d._status === 400 || d._status === 409,
|
||||
d = await T.apiUpload('/admin/packages/install', blob, 'evil.surface');
|
||||
T.assert(d._status === 400 || d._status === 422,
|
||||
'path traversal surface accepted! got ' + d._status);
|
||||
} catch (e) {
|
||||
if (e.message.indexOf('502') !== -1) {
|
||||
throw new Error('INCONCLUSIVE (502): proxy returned 502 on surface upload');
|
||||
}
|
||||
T.assert(e.message.indexOf('400') !== -1 || e.message.indexOf('409') !== -1 || e.message.indexOf('415') !== -1,
|
||||
T.assert(e.message.indexOf('400') !== -1 || e.message.indexOf('422') !== -1 || e.message.indexOf('415') !== -1,
|
||||
'unexpected error: ' + e.message);
|
||||
} finally {
|
||||
// Cleanup: if the evil package was somehow installed, delete it
|
||||
if (d && (d._status === 200 || d._status === 201) && d.id) {
|
||||
T.registerCleanup(function () { return T.safeDelete('/admin/packages/' + d.id); });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -134,7 +134,17 @@
|
||||
for (const wf of list) {
|
||||
if (wf.slug) installed[wf.slug] = wf;
|
||||
}
|
||||
} catch (e) { /* ignore — show all as not-installed */ }
|
||||
} catch (e) {
|
||||
const errDiv = document.createElement('div');
|
||||
errDiv.className = 'sw-inline-error';
|
||||
errDiv.innerHTML = '<span>' + _esc(e.message || 'Failed to load workflows') + '</span>';
|
||||
const retryBtn = document.createElement('button');
|
||||
retryBtn.className = 'sw-btn sw-btn--secondary sw-btn--sm';
|
||||
retryBtn.textContent = 'Retry';
|
||||
retryBtn.onclick = () => { errDiv.remove(); _mount(surface); };
|
||||
errDiv.appendChild(retryBtn);
|
||||
grid.appendChild(errDiv);
|
||||
}
|
||||
|
||||
for (const wf of WORKFLOWS) {
|
||||
grid.appendChild(_buildCard(wf, installed[wf.slug]));
|
||||
|
||||
Reference in New Issue
Block a user