ui: add community overlay slots

This commit is contained in:
github-ops
2026-05-15 16:55:50 +00:00
parent 42312ff76f
commit 3eeac67634
8 changed files with 178 additions and 6 deletions
+2 -1
View File
@@ -45,7 +45,7 @@ Verification:
- UI build and Community-targeted smoke/e2e pass.
Progress:
- done:
- done:
- `deploy/community/*` already acts as the canonical Community delivery contour
- Community capability model is already constrained to:
- `REST`
@@ -170,4 +170,5 @@ Progress:
- Phase 3 dependency slice: added public billing seam in `crank-core``BillingHook`, `BillingGate`, `BillingError`, `SharedBillingHook`, and `NoopBillingHook` — so `cloud` can layer billing policy without private-only contracts in the base repo
- Phase 3 runtime slice: `RuntimeRequestContext` now carries optional metering context (`workspace_id`, `agent_id`, `source`), `RuntimeExecutorBuilder` accepts a `MeteringSink`, and `RuntimeExecutor` emits `MeteringEvent` on invocation completion while Community apps keep using the default `NoopMeteringSink`
- Phase 3 dependency slice: added `CacheBackendFactory` in `crank-runtime` with `BuiltinCacheBackendFactory`; unlike the original draft, the seam lives in runtime rather than core to avoid a `crank-core -> crank-runtime` dependency cycle around `RuntimeCacheStores`
- Phase 5 / task `5.1`: added Community UI overlay foundation — `slot-registry.js`, `overlay-loader.js`, optional `CRANK_UI_OVERLAY_DIR` copy path in the UI build, settings slot mounts, and wizard protocol-card slot mounts; Community build stays noop without any private overlay package
- backward-compatible alias `CommunityMachineCredentialVerifier` сохранен, поведение Community не изменено
+5
View File
@@ -277,6 +277,11 @@
</div>
</div>
</div>
<div data-slot="settings.sso_panel"></div>
<div data-slot="settings.totp_panel"></div>
<div data-slot="settings.audit_panel"></div>
<div data-slot="settings.billing_panel"></div>
</div>
<div class="section-card" style="margin-bottom: 20px;">
+5
View File
@@ -34,6 +34,11 @@
</div>
</div>
<div data-slot="wizard.protocol_cards.graphql"></div>
<div data-slot="wizard.protocol_cards.grpc"></div>
<div data-slot="wizard.protocol_cards.soap"></div>
<div data-slot="wizard.protocol_cards.websocket"></div>
</div><!-- /protocol-grid -->
+75
View File
@@ -0,0 +1,75 @@
(function() {
var manifestPromise = null;
var loadedScripts = Object.create(null);
function overlayBasePath() {
return (window.APP_BASE || '/') + 'overlay/';
}
function fetchManifest() {
return fetch(overlayBasePath() + 'manifest.json', { cache: 'no-store' })
.then(function(response) {
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error('overlay manifest request failed');
}
return response.json();
})
.catch(function() {
return null;
});
}
function loadScript(path) {
if (loadedScripts[path]) {
return loadedScripts[path];
}
loadedScripts[path] = new Promise(function(resolve, reject) {
var script = document.createElement('script');
script.src = overlayBasePath() + path.replace(/^\/+/, '');
script.async = false;
script.onload = function() { resolve(); };
script.onerror = function() { reject(new Error('overlay script load failed')); };
document.head.appendChild(script);
}).catch(function() {
return null;
});
return loadedScripts[path];
}
function load() {
if (manifestPromise) {
return manifestPromise;
}
manifestPromise = fetchManifest().then(function(manifest) {
if (!(manifest && manifest.slots && window.CrankUiSlots)) {
return null;
}
var tasks = Object.entries(manifest.slots)
.filter(function(entry) {
return window.CrankUiSlots.isAllowed(entry[0]) && typeof entry[1] === 'string';
})
.map(function(entry) {
return loadScript(entry[1]);
});
return Promise.all(tasks).then(function() {
return manifest;
});
});
return manifestPromise;
}
async function render(root, context) {
await load();
if (window.CrankUiSlots) {
window.CrankUiSlots.renderAll(root, context);
}
}
window.CrankOverlay = {
load: load,
render: render,
};
}());
+22 -4
View File
@@ -193,6 +193,19 @@ async function loadCapabilities() {
}
}
async function renderOverlaySlots() {
if (!window.CrankOverlay || typeof window.CrankOverlay.render !== 'function') {
return;
}
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
await window.CrankOverlay.render(document, {
workspace: window.getCurrentWorkspace ? window.getCurrentWorkspace() : null,
capabilities: settingsCapabilities,
locale: localStorage.getItem('crank_lang') || 'en',
});
}
function bindProfileSave() {
var button = document.getElementById('settings-profile-save-btn');
if (!button || button.dataset.bound === 'true') {
@@ -418,12 +431,17 @@ async function loadWorkspaceSettings() {
}
}
document.addEventListener('DOMContentLoaded', function () {
async function initSettingsPage() {
injectLanguageSwitcher();
bindSectionNavigation();
loadProfile();
loadCapabilities();
await loadProfile();
await loadCapabilities();
bindProfileSave();
bindPasswordSave();
loadWorkspaceSettings();
await loadWorkspaceSettings();
await renderOverlaySlots();
}
document.addEventListener('DOMContentLoaded', function () {
void initSettingsPage();
});
+55
View File
@@ -0,0 +1,55 @@
(function() {
var allowedSlots = {
'settings.sso_panel': true,
'settings.totp_panel': true,
'settings.audit_panel': true,
'settings.billing_panel': true,
'wizard.protocol_cards.graphql': true,
'wizard.protocol_cards.grpc': true,
'wizard.protocol_cards.soap': true,
'wizard.protocol_cards.websocket': true,
};
var handlers = Object.create(null);
function noop() {}
function isAllowed(slotId) {
return !!allowedSlots[slotId];
}
function register(slotId, render) {
if (!isAllowed(slotId) || typeof render !== 'function') {
return false;
}
handlers[slotId] = render;
return true;
}
function renderInto(target, slotId, context) {
if (!(target && slotId && isAllowed(slotId))) {
return;
}
var handler = handlers[slotId] || noop;
handler(target, context || {});
}
function renderAll(root, context) {
var scope = root || document;
scope.querySelectorAll('[data-slot]').forEach(function(target) {
renderInto(target, target.getAttribute('data-slot'), context);
});
}
function knownSlots() {
return Object.keys(allowedSlots);
}
window.CrankUiSlots = {
isAllowed: isAllowed,
knownSlots: knownSlots,
register: register,
renderAll: renderAll,
renderInto: renderInto,
};
}());
+8 -1
View File
@@ -86,9 +86,16 @@ async function initWizardPage() {
await loadProtocolCapabilities();
await loadWizardPanels([1, 2, 3, 4, 5]);
applyEditionProtocolVisibility(editionCapabilities);
if (window.CrankOverlay && typeof window.CrankOverlay.render === 'function') {
await window.CrankOverlay.render(document, {
workspace: workspace,
capabilities: editionCapabilities,
locale: localStorage.getItem('crank_lang') || 'en',
});
}
await loadWizardAuthResources();
bindProtocolCards();
applyEditionProtocolVisibility(editionCapabilities);
bindWizardLiveActions();
updateUpstreamAuthUi();
renderEditionCapabilityHints(editionCapabilities);
+6
View File
@@ -16,6 +16,8 @@ const BUNDLES = {
files: [
'js/config.js',
'js/i18n.js',
'js/slot-registry.js',
'js/overlay-loader.js',
'js/dom.js',
'js/diagnostics.js',
'js/workspace.js',
@@ -176,6 +178,7 @@ function rewriteHtml(relativePath, replacements, prefix) {
}
async function main() {
var overlayDirectory = process.env.CRANK_UI_OVERLAY_DIR || '';
removeDirectory(DIST_DIR);
ensureDirectory(BUNDLE_DIR);
@@ -199,6 +202,9 @@ async function main() {
});
copyDirectory(path.join(ROOT_DIR, 'icons'), path.join(DIST_DIR, 'icons'));
copyFile(resolveBrandImage(), path.join(DIST_DIR, 'Crank.png'));
if (overlayDirectory && fs.existsSync(overlayDirectory)) {
copyDirectory(overlayDirectory, path.join(DIST_DIR, 'overlay'));
}
rewriteHtml('index.html', manifest, '');
rewriteHtml('html/login.html', manifest, '..');