feat: complete Epic 1 production foundation

This commit is contained in:
2026-08-25 01:24:11 +03:00
parent 767428436d
commit 182bde8ac0
298 changed files with 35719 additions and 5299 deletions
+488
View File
@@ -0,0 +1,488 @@
(function() {
var STEP_ORDER = ['operation', 'test', 'publish_operation', 'agent', 'key', 'mcp_connection', 'first_call'];
var state = {
epoch: 0,
workspaceId: null,
snapshot: null,
loading: false,
error: null,
open: false,
dismissed: false,
controller: null,
refreshPromise: null,
refreshQueued: false,
startedRequested: false,
};
var channel = null;
var trigger = null;
var panel = null;
var heading = null;
var subtitle = null;
var collapseButton = null;
var lastFocused = null;
function tKey(key) { return window.t ? window.t(key) : key; }
function currentWorkspace() { return window.getCurrentWorkspace ? window.getCurrentWorkspace() : null; }
function preferenceKey() { return 'crank_onboarding_ui:' + (state.workspaceId || 'none'); }
function safePreference() {
try { return JSON.parse(localStorage.getItem(preferenceKey())) || {}; } catch (_error) { return {}; }
}
function writePreference(value) {
try { localStorage.setItem(preferenceKey(), JSON.stringify(value)); } catch (_error) {}
}
function updatePreference(value) {
writePreference(Object.assign({}, safePreference(), value));
}
function injectStylesheet() {
if (document.querySelector('link[data-crank-onboarding-css]')) return;
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = (window.APP_BASE || '/') + 'css/onboarding.css';
link.dataset.crankOnboardingCss = 'true';
document.head.appendChild(link);
}
function button(label, testId, handler, className) {
var node = document.createElement('button');
node.type = 'button';
node.textContent = label;
if (testId) node.dataset.testid = testId;
node.className = className || 'onboarding-icon-button';
node.addEventListener('click', handler);
return node;
}
function normalizeStep(raw) {
var id = raw.id || raw.kind;
if (id === 'publish') id = 'publish_operation';
if (id === 'connection') id = 'mcp_connection';
var completed = raw.completed === true || raw.status === 'complete';
return {
id: id,
completed: completed,
status: raw.status || (completed ? 'complete' : 'pending'),
actionCode: raw.action_code || raw.action || '',
reasonCode: raw.reason_code || '',
};
}
function normalizedSteps() {
var raw = state.snapshot && Array.isArray(state.snapshot.steps) ? state.snapshot.steps : [];
var mapped = raw.map(normalizeStep);
return STEP_ORDER.map(function(id) {
return mapped.find(function(item) { return item.id === id; }) || { id: id, completed: false, status: 'pending', actionCode: '', reasonCode: '' };
});
}
function completedCount() { return normalizedSteps().filter(function(step) { return step.completed; }).length; }
function currentStep(steps) {
return steps.find(function(step) { return !step.completed && step.status === 'current'; })
|| steps.find(function(step) { return !step.completed; }) || null;
}
function stepLabel(id) { return tKey('onboarding.step.' + id); }
function actionLabel(id) { return tKey('onboarding.action.' + id); }
function deepLink(step) {
var snapshot = state.snapshot || {};
var returnTo = encodeURIComponent(window.location.pathname + window.location.search);
if (step.id === 'operation') return '/wizard/?onboarding=1&return=' + returnTo;
if (step.id === 'test' || step.id === 'publish_operation') {
if (!snapshot.operation_id) return '/wizard/?onboarding=1&return=' + returnTo;
return '/wizard/?mode=edit&operationId=' + encodeURIComponent(snapshot.operation_id)
+ '&onboarding=1&step=5&return=' + returnTo;
}
if (step.id === 'agent') {
return '/agents?onboarding=1&action=create&operationId=' + encodeURIComponent(snapshot.operation_id || '')
+ '&operationVersion=' + encodeURIComponent(snapshot.operation_version || '') + '&return=' + returnTo;
}
if (step.id === 'key' || step.id === 'mcp_connection') {
return '/api-keys?onboarding=1&action=create&agentId=' + encodeURIComponent(snapshot.agent_id || '')
+ '&agentRevision=' + encodeURIComponent(snapshot.catalog_revision || '') + '&return=' + returnTo;
}
if (step.id === 'first_call' && snapshot.first_call && snapshot.first_call.log_id) {
return '/logs?log_id=' + encodeURIComponent(snapshot.first_call.log_id);
}
return '/logs?agent_id=' + encodeURIComponent(snapshot.agent_id || '');
}
function navigate(step) { window.location.href = deepLink(step); }
function recordStarted() {
if (state.startedRequested || !state.snapshot || state.snapshot.completed) return;
state.startedRequested = true;
recordPresentation('started', { stable: true }).then(function(response) {
if (!response) state.startedRequested = false;
});
}
function setOpen(open) {
state.open = open;
panel.hidden = !open;
trigger.setAttribute('aria-expanded', String(open));
if (open) {
lastFocused = document.activeElement;
render();
var focusTarget = panel.querySelector('[aria-current="step"] .onboarding-step-action') || panel.querySelector('button');
if (focusTarget) focusTarget.focus();
recordStarted();
} else if (lastFocused && typeof lastFocused.focus === 'function') {
lastFocused.focus();
}
}
function render() {
if (!trigger || !panel) return;
var count = completedCount();
trigger.textContent = state.dismissed
? tKey('onboarding.resume')
: tKey('onboarding.title') + ' · ' + count + '/7';
trigger.setAttribute('aria-label', state.dismissed ? tKey('onboarding.resume') : trigger.textContent);
if (heading) heading.textContent = tKey('onboarding.title');
if (subtitle) subtitle.textContent = tKey('onboarding.subtitle');
if (collapseButton) collapseButton.setAttribute('aria-label', tKey('onboarding.collapse'));
var body = panel.querySelector('.onboarding-body');
var hadActionFocus = state.open
&& document.activeElement
&& body.contains(document.activeElement)
&& document.activeElement.classList.contains('onboarding-step-action');
body.replaceChildren();
var status = document.createElement('div');
status.className = 'onboarding-status';
status.setAttribute('role', 'status');
status.setAttribute('aria-live', 'polite');
status.id = 'crank-onboarding-progress';
status.textContent = state.loading ? tKey('onboarding.loading') : tKey('onboarding.progress').replace('{count}', String(count));
body.appendChild(status);
if (state.error) {
var error = document.createElement('div');
error.className = 'onboarding-error';
error.setAttribute('role', 'alert');
var message = document.createElement('div');
message.textContent = tKey('onboarding.error');
error.appendChild(message);
var errorCode = state.error.code
|| (state.error.payload && state.error.payload.error && (state.error.payload.error.code || state.error.payload.error.error_code))
|| (state.error.payload && (state.error.payload.code || state.error.payload.error_code));
var support = [
errorCode ? tKey('onboarding.error_code') + ': ' + errorCode : '',
state.error.requestId ? tKey('onboarding.request_id') + ': ' + state.error.requestId : '',
state.error.traceId ? tKey('onboarding.trace_id') + ': ' + state.error.traceId : '',
].filter(Boolean);
if (support.length) {
var supportIds = document.createElement('div');
supportIds.className = 'onboarding-error-ids';
supportIds.textContent = support.join(' · ');
error.appendChild(supportIds);
}
error.appendChild(button(tKey('onboarding.retry'), 'onboarding-retry', refresh, 'onboarding-step-action'));
body.appendChild(error);
if (state.focusError && state.open) {
state.focusError = false;
var retry = error.querySelector('[data-testid="onboarding-retry"]');
if (retry) retry.focus();
}
return;
}
if (!state.snapshot) return;
var steps = normalizedSteps();
var active = currentStep(steps);
if (state.snapshot.completed || !active) {
var complete = document.createElement('div');
complete.className = 'onboarding-completion';
complete.dataset.testid = 'onboarding-completion';
complete.textContent = tKey('onboarding.completed');
var firstCall = state.snapshot.first_call;
if (firstCall) {
var evidence = document.createElement('dl');
evidence.className = 'onboarding-first-call-evidence';
[
[tKey('onboarding.tool'), firstCall.tool_name],
[tKey('onboarding.timestamp'), firstCall.occurred_at],
[tKey('onboarding.request_id'), firstCall.request_id],
[tKey('onboarding.trace_id'), firstCall.trace_id],
].forEach(function(entry) {
if (!entry[1]) return;
var term = document.createElement('dt');
term.textContent = entry[0];
var value = document.createElement('dd');
value.textContent = entry[1];
evidence.appendChild(term);
evidence.appendChild(value);
});
complete.appendChild(evidence);
if (firstCall.log_id) {
var link = document.createElement('a');
link.href = '/logs?log_id=' + encodeURIComponent(firstCall.log_id);
link.textContent = tKey('onboarding.open_invocation');
complete.appendChild(link);
}
}
body.appendChild(complete);
} else {
var list = document.createElement('ol');
list.className = 'onboarding-list';
list.setAttribute('aria-label', tKey('onboarding.progress_label'));
list.setAttribute('aria-describedby', 'crank-onboarding-progress');
steps.forEach(function(step, index) {
var item = document.createElement('li');
item.className = 'onboarding-step' + (step.completed ? ' is-complete' : '') + (step.status === 'regressed' ? ' is-regressed' : '');
if (step === active) item.setAttribute('aria-current', 'step');
var marker = document.createElement('span');
marker.className = 'onboarding-step-marker';
marker.textContent = step.completed ? '✓' : String(index + 1);
marker.setAttribute('aria-hidden', 'true');
var content = document.createElement('div');
var title = document.createElement('div');
title.className = 'onboarding-step-title';
title.textContent = stepLabel(step.id);
content.appendChild(title);
if (step === active) {
var reason = document.createElement('div');
reason.className = 'onboarding-step-reason';
reason.textContent = tKey(step.status === 'regressed' ? 'onboarding.regressed' : 'onboarding.action_needed');
content.appendChild(reason);
content.appendChild(button(actionLabel(step.id), null, function() { navigate(step); }, 'onboarding-step-action'));
}
item.appendChild(marker);
item.appendChild(content);
list.appendChild(item);
});
body.appendChild(list);
}
var footer = document.createElement('div');
footer.className = 'onboarding-footer';
footer.appendChild(button(tKey('onboarding.refresh'), 'onboarding-refresh', refresh, 'onboarding-link-button'));
footer.appendChild(button(tKey('onboarding.dismiss'), 'onboarding-dismiss', dismiss, 'onboarding-link-button'));
body.appendChild(footer);
if (hadActionFocus) {
var replacementAction = panel.querySelector('[aria-current="step"] .onboarding-step-action');
if (replacementAction) replacementAction.focus();
}
}
function refresh() {
var workspace = currentWorkspace();
var workspaceId = workspace ? workspace.id : null;
if (state.refreshPromise && workspaceId === state.workspaceId) {
state.refreshQueued = true;
return state.refreshPromise;
}
if (workspaceId !== state.workspaceId) state.refreshQueued = false;
var promise = performRefresh(workspaceId);
state.refreshPromise = promise;
promise.finally(function() {
if (state.refreshPromise !== promise) return;
state.refreshPromise = null;
if (state.refreshQueued) {
state.refreshQueued = false;
refresh();
}
});
return promise;
}
async function performRefresh(workspaceId) {
var workspaceChanged = workspaceId !== state.workspaceId;
var epoch = ++state.epoch;
if (state.controller) state.controller.abort();
state.controller = typeof AbortController === 'function' ? new AbortController() : null;
state.workspaceId = workspaceId;
if (workspaceChanged) {
state.dismissed = Boolean(safePreference().dismissed);
state.startedRequested = false;
if (state.open) setOpen(false);
}
state.loading = true;
state.error = null;
state.focusError = false;
render();
if (!workspaceId || !window.CrankApi || typeof window.CrankApi.getOnboarding !== 'function') {
state.loading = false;
state.error = new Error(tKey('onboarding.error'));
render();
return;
}
try {
var snapshot = await window.CrankApi.getOnboarding(workspaceId);
if (epoch !== state.epoch || workspaceId !== (currentWorkspace() && currentWorkspace().id)) return;
state.snapshot = snapshot;
var preference = safePreference();
if (window.location.pathname === '/'
&& !preference.opened
&& !preference.dismissed
&& !snapshot.completed) {
updatePreference({ opened: true });
setOpen(true);
}
renderDeepLinkRecovery(snapshot);
} catch (error) {
if (epoch !== state.epoch) return;
state.error = error;
state.focusError = state.open;
} finally {
if (epoch === state.epoch) {
state.loading = false;
render();
}
}
}
async function recordPresentation(eventName, options) {
if (!state.snapshot || !state.workspaceId || !window.CrankApi.recordOnboardingEvent) return;
var expectedRevision = Number(state.snapshot.revision || 0);
var expectedEpoch = state.epoch;
var expectedWorkspaceId = state.workspaceId;
var key = options && options.stable
? ['ui', eventName, state.workspaceId, 'v1'].join(':')
: ['ui', eventName, state.workspaceId, expectedRevision].join(':');
try {
var response = await window.CrankApi.recordOnboardingEvent(state.workspaceId, {
event: eventName,
idempotency_key: key,
expected_revision: expectedRevision,
}, options && options.keepalive ? { keepalive: true } : null);
var currentRevision = Number(state.snapshot && state.snapshot.revision || 0);
if (response
&& expectedEpoch === state.epoch
&& expectedWorkspaceId === state.workspaceId
&& expectedWorkspaceId === (currentWorkspace() && currentWorkspace().id)
&& currentRevision === expectedRevision) {
state.snapshot = response;
render();
}
return response;
} catch (error) {
if (error.status === 409) await refresh();
return null;
}
}
async function dismiss() {
updatePreference({ dismissed: true, opened: true });
state.dismissed = true;
setOpen(false);
render();
await recordPresentation('dismissed');
await recordPresentation('abandoned', { stable: true });
}
function resume() {
updatePreference({ dismissed: false, opened: true });
state.dismissed = false;
setOpen(true);
recordPresentation('resumed');
}
function renderDeepLinkRecovery(snapshot) {
var params = new URLSearchParams(window.location.search);
if (params.get('onboarding') !== '1') return;
var stale = false;
if (window.location.pathname === '/agents') {
var expectedOperation = params.get('operationId') || '';
var expectedVersion = Number(params.get('operationVersion') || 0);
stale = Boolean(expectedOperation) && (
snapshot.operation_id !== expectedOperation
|| (expectedVersion && snapshot.operation_version !== expectedVersion)
);
} else if (window.location.pathname === '/api-keys') {
var expectedAgent = params.get('agentId') || '';
var expectedRevision = Number(params.get('agentRevision') || 0);
stale = Boolean(expectedAgent) && (
snapshot.agent_id !== expectedAgent
|| (expectedRevision && snapshot.catalog_revision !== expectedRevision)
);
}
var existing = document.getElementById('onboarding-deep-link-stale');
if (!stale) {
if (existing) existing.remove();
return;
}
if (existing) return;
var recovery = document.createElement('div');
recovery.id = 'onboarding-deep-link-stale';
recovery.className = 'onboarding-deep-link-stale';
recovery.setAttribute('role', 'alert');
recovery.dataset.testid = 'onboarding-deep-link-stale';
var message = document.createElement('div');
message.textContent = tKey('onboarding.deep_link_stale');
recovery.appendChild(message);
recovery.appendChild(button(tKey('onboarding.reselect'), 'onboarding-reselect', async function() {
var resetButton = this;
resetButton.disabled = true;
try {
if (window.CrankApi && typeof window.CrankApi.resetOnboardingSelection === 'function') {
await window.CrankApi.resetOnboardingSelection(state.workspaceId, Number(snapshot.revision || 0));
}
var clean = new URLSearchParams(window.location.search);
['onboarding', 'action', 'operationId', 'operationVersion', 'agentId', 'agentRevision', 'return'].forEach(function(key) {
clean.delete(key);
});
window.location.replace(window.location.pathname + (clean.toString() ? '?' + clean.toString() : ''));
} catch (error) {
resetButton.disabled = false;
state.error = error;
if (!state.open) setOpen(true);
render();
}
}, 'onboarding-step-action'));
document.body.appendChild(recovery);
}
function mount() {
if (document.getElementById('crank-onboarding-root')) return;
injectStylesheet();
var root = document.createElement('div');
root.id = 'crank-onboarding-root';
trigger = button(tKey('onboarding.title'), 'onboarding-trigger', function() {
if (state.dismissed) {
resume();
return;
}
setOpen(!state.open);
}, 'onboarding-trigger');
trigger.setAttribute('aria-controls', 'crank-onboarding-panel');
trigger.setAttribute('aria-expanded', 'false');
panel = document.createElement('aside');
panel.id = 'crank-onboarding-panel';
panel.className = 'onboarding-panel';
panel.dataset.testid = 'onboarding-checklist';
panel.setAttribute('aria-labelledby', 'crank-onboarding-title');
panel.hidden = true;
var head = document.createElement('div');
head.className = 'onboarding-head';
var headingWrap = document.createElement('div');
heading = document.createElement('h2');
heading.id = 'crank-onboarding-title';
heading.className = 'onboarding-title';
heading.textContent = tKey('onboarding.title');
subtitle = document.createElement('p');
subtitle.className = 'onboarding-subtitle';
subtitle.textContent = tKey('onboarding.subtitle');
headingWrap.appendChild(heading);
headingWrap.appendChild(subtitle);
var actions = document.createElement('div');
actions.className = 'onboarding-head-actions';
collapseButton = button('', 'onboarding-collapse', function() { setOpen(false); }, 'onboarding-icon-button');
collapseButton.setAttribute('aria-label', tKey('onboarding.collapse'));
actions.appendChild(collapseButton);
head.appendChild(headingWrap);
head.appendChild(actions);
var body = document.createElement('div');
body.className = 'onboarding-body';
panel.appendChild(head);
panel.appendChild(body);
root.appendChild(trigger);
root.appendChild(panel);
document.body.appendChild(root);
refresh();
}
function signalRefresh() {
if (channel) channel.postMessage({ type: 'refresh' });
try { localStorage.setItem('crank_onboarding_signal', String(Date.now())); } catch (_error) {}
}
window.CrankOnboarding = { refresh: refresh, signalRefresh: signalRefresh };
document.addEventListener('DOMContentLoaded', async function() {
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
mount();
if (typeof BroadcastChannel === 'function') {
channel = new BroadcastChannel('crank-onboarding');
channel.addEventListener('message', function(event) {
if (event && event.data && event.data.type !== 'refresh') return;
refresh();
});
}
});
window.addEventListener('crank:workspacechange', function() {
state.snapshot = null;
refresh();
});
window.addEventListener('crank:sessionchange', refresh);
window.addEventListener('crank:langchange', function() { render(); });
window.addEventListener('storage', function(event) { if (event.key === 'crank_onboarding_signal') refresh(); });
document.addEventListener('visibilitychange', function() { if (!document.hidden) refresh(); });
document.addEventListener('keydown', function(event) { if (event.key === 'Escape' && state.open) setOpen(false); });
}());