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
+123 -18
View File
@@ -20,6 +20,7 @@ function mapAgent(agent) {
created_at: agent.created_at,
current_draft_version: agent.current_draft_version || 1,
latest_published_version: agent.latest_published_version,
catalog_revision: typeof agent.catalog_revision === 'number' ? agent.catalog_revision : 0,
mcp_endpoint: agent.mcp_endpoint || '',
};
@@ -60,6 +61,7 @@ document.addEventListener('alpine:init', function() {
drawerMode: 'create',
editingId: null,
saving: false,
lifecycleBusyId: null,
form: {
display_name: '',
@@ -79,6 +81,9 @@ document.addEventListener('alpine:init', function() {
searchPreviewItems: [],
searchPreviewLoading: false,
searchPreviewRan: false,
_loadGeneration: 0,
_mutationGeneration: 0,
_queryHydrated: false,
async init() {
var self = this;
@@ -88,6 +93,7 @@ document.addEventListener('alpine:init', function() {
this.workspaceId = workspace ? workspace.id : null;
await this.loadCapabilities();
await this.reload();
this.hydrateOnboardingQuery();
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && self.drawerOpen) {
@@ -100,6 +106,9 @@ document.addEventListener('alpine:init', function() {
});
window.addEventListener('crank:workspacechange', async function(event) {
self._loadGeneration += 1;
self._mutationGeneration += 1;
self._queryHydrated = false;
self.workspaceId = event.detail ? event.detail.id : null;
await self.loadCapabilities();
await self.reload();
@@ -119,6 +128,8 @@ document.addEventListener('alpine:init', function() {
},
async reload() {
var generation = ++this._loadGeneration;
var workspaceId = this.workspaceId;
this.loading = true;
this.loadError = '';
@@ -132,18 +143,39 @@ document.addEventListener('alpine:init', function() {
try {
var responses = await Promise.all([
window.CrankApi.listAgents(this.workspaceId),
window.CrankApi.listOperations(this.workspaceId),
window.CrankApi.listAgents(workspaceId),
window.CrankApi.listOperations(workspaceId),
]);
if (generation !== this._loadGeneration || workspaceId !== this.workspaceId) return;
this.agents = ((responses[0] && responses[0].items) || []).map(mapAgent);
this.operations = ((responses[1] && responses[1].items) || []).map(mapOperation);
} catch (error) {
if (generation !== this._loadGeneration || workspaceId !== this.workspaceId) return;
this.agents = [];
this.operations = [];
this.loadError = error.message || this.tKey('agents.error.load');
}
this.loading = false;
if (generation === this._loadGeneration && workspaceId === this.workspaceId) this.loading = false;
},
hydrateOnboardingQuery() {
if (this._queryHydrated) return;
this._queryHydrated = true;
var params = new URLSearchParams(window.location.search);
if (params.get('onboarding') !== '1' || params.get('action') !== 'create') return;
var operationId = params.get('operationId') || '';
var expectedVersion = Number(params.get('operationVersion') || 0);
var operation = this.operations.find(function(item) { return item.id === operationId; });
this.openCreate();
if (operation && operation.latest_published_version
&& (!expectedVersion || operation.latest_published_version === expectedVersion)) {
this.form.selectedOps = [operation.id];
}
setTimeout(function() {
var input = document.querySelector('.drawer input.form-input');
if (input) input.focus();
}, 0);
},
get filteredAgents() {
@@ -254,6 +286,9 @@ document.addEventListener('alpine:init', function() {
accessMode: 'direct',
groups: [],
searchMaxResults: 8,
catalogRevision: 0,
currentDraftVersion: 1,
latestPublishedVersion: null,
};
this.opSearch = '';
this.slugManuallyEdited = false;
@@ -283,6 +318,9 @@ document.addEventListener('alpine:init', function() {
searchMaxResults: policy.search && policy.search.max_results
? policy.search.max_results
: 8,
catalogRevision: agent.catalog_revision || 0,
currentDraftVersion: agent.current_draft_version || 1,
latestPublishedVersion: agent.latest_published_version,
};
this.opSearch = '';
this.slugManuallyEdited = true;
@@ -293,6 +331,7 @@ document.addEventListener('alpine:init', function() {
closeDrawer() {
this.drawerOpen = false;
this.saving = false;
this.lifecycleBusyId = null;
},
onNameInput(value) {
@@ -514,6 +553,8 @@ document.addEventListener('alpine:init', function() {
}
this.saving = true;
var mutationGeneration = ++this._mutationGeneration;
var workspaceId = this.workspaceId;
try {
var agentId = this.editingId;
@@ -524,7 +565,7 @@ document.addEventListener('alpine:init', function() {
var previousStatus = existingAgent ? (existingAgent.raw_status || 'draft') : 'draft';
if (this.drawerMode === 'create') {
var created = await window.CrankApi.createAgent(this.workspaceId, {
var created = await window.CrankApi.createAgent(workspaceId, {
slug: this.form.slug,
display_name: this.form.display_name,
description: this.form.description,
@@ -534,17 +575,17 @@ document.addEventListener('alpine:init', function() {
agentId = created.agent_id;
currentVersion = created.version || 1;
} else {
await window.CrankApi.updateAgent(this.workspaceId, this.editingId, {
await window.CrankApi.updateAgent(workspaceId, this.editingId, {
slug: this.form.slug,
display_name: this.form.display_name,
description: this.form.description,
});
var agent = await window.CrankApi.getAgent(this.workspaceId, this.editingId);
var agent = await window.CrankApi.getAgent(workspaceId, this.editingId);
currentVersion = agent.current_draft_version || 1;
}
var savedVersion = await window.CrankApi.saveAgentBindings(
this.workspaceId,
workspaceId,
agentId,
{
bindings: this.agentBindings(),
@@ -554,16 +595,19 @@ document.addEventListener('alpine:init', function() {
currentVersion = savedVersion.version || currentVersion;
if (this.form.status === 'published') {
await window.CrankApi.publishAgent(this.workspaceId, agentId, {
await window.CrankApi.publishAgent(workspaceId, agentId, {
version: currentVersion,
});
} else if (this.form.status === 'archived') {
await window.CrankApi.archiveAgent(this.workspaceId, agentId);
await window.CrankApi.archiveAgent(workspaceId, agentId);
} else if (this.drawerMode === 'edit' && previousStatus !== 'draft') {
await window.CrankApi.unpublishAgent(this.workspaceId, agentId);
await window.CrankApi.unpublishAgent(workspaceId, agentId);
}
if (mutationGeneration !== this._mutationGeneration || workspaceId !== this.workspaceId) return;
await this.reload();
if (mutationGeneration !== this._mutationGeneration || workspaceId !== this.workspaceId) return;
if (window.CrankUi) {
window.CrankUi.success(
this.tfKey('agents.toast.saved_message', {
@@ -576,10 +620,12 @@ document.addEventListener('alpine:init', function() {
);
}
this.closeDrawer();
if (window.CrankOnboarding) window.CrankOnboarding.signalRefresh();
} catch (error) {
if (mutationGeneration !== this._mutationGeneration || workspaceId !== this.workspaceId) return;
if (window.CrankUi) {
window.CrankUi.error(
error.message || this.tKey('agents.toast.save_error_message'),
this.agentMutationErrorMessage(error, this.tKey('agents.toast.save_error_message')),
this.tKey('agents.toast.save_error_title')
);
}
@@ -588,9 +634,11 @@ document.addEventListener('alpine:init', function() {
},
async deleteAgent(id) {
if (this.lifecycleBusyId) return;
if (!confirm(this.tKey('agents.toast.delete_confirm'))) return;
try {
this.lifecycleBusyId = id;
var agent = this.agents.find(function(item) { return item.id === id; });
await window.CrankApi.deleteAgent(this.workspaceId, id);
await this.reload();
@@ -603,29 +651,44 @@ document.addEventListener('alpine:init', function() {
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || this.tKey('agents.toast.delete_error_message'),
this.agentMutationErrorMessage(error, this.tKey('agents.toast.delete_error_message')),
this.tKey('agents.toast.delete_error_title')
);
}
} finally {
this.lifecycleBusyId = null;
}
},
async applyLifecycle(agent, action) {
if (!this.workspaceId || !window.CrankApi) {
if (!this.workspaceId || !window.CrankApi || this.lifecycleBusyId) {
return;
}
var confirmKey = action === 'publish'
? 'agents.toast.lifecycle_publish_confirm'
: action === 'unpublish'
? 'agents.toast.lifecycle_unpublish_confirm'
: 'agents.toast.lifecycle_archive_confirm';
if (!confirm(this.tfKey(confirmKey, { name: agent.display_name }))) {
return;
}
try {
var mutationGeneration = ++this._mutationGeneration;
var workspaceId = this.workspaceId;
this.lifecycleBusyId = agent.id;
if (action === 'publish') {
await window.CrankApi.publishAgent(this.workspaceId, agent.id, {
await window.CrankApi.publishAgent(workspaceId, agent.id, {
version: agent.current_draft_version || 1,
});
} else if (action === 'unpublish') {
await window.CrankApi.unpublishAgent(this.workspaceId, agent.id);
await window.CrankApi.unpublishAgent(workspaceId, agent.id);
} else if (action === 'archive') {
await window.CrankApi.archiveAgent(this.workspaceId, agent.id);
await window.CrankApi.archiveAgent(workspaceId, agent.id);
}
if (mutationGeneration !== this._mutationGeneration || workspaceId !== this.workspaceId) return;
await this.reload();
if (mutationGeneration !== this._mutationGeneration || workspaceId !== this.workspaceId) return;
if (window.CrankUi) {
window.CrankUi.success(
action === 'publish'
@@ -636,13 +699,17 @@ document.addEventListener('alpine:init', function() {
this.tKey('agents.toast.lifecycle_title')
);
}
if (window.CrankOnboarding) window.CrankOnboarding.signalRefresh();
} catch (error) {
if (workspaceId !== this.workspaceId) return;
if (window.CrankUi) {
window.CrankUi.error(
error.message || this.tKey('agents.toast.lifecycle_error_message'),
this.agentMutationErrorMessage(error, this.tKey('agents.toast.lifecycle_error_message')),
this.tKey('agents.toast.lifecycle_error_title')
);
}
} finally {
this.lifecycleBusyId = null;
}
},
@@ -651,7 +718,7 @@ document.addEventListener('alpine:init', function() {
return { key: 'unpublish', label: this.tKey('agents.lifecycle.unpublish') };
}
if (agent.raw_status === 'archived') {
return { key: 'unpublish', label: this.tKey('agents.lifecycle.restore_draft') };
return { key: 'archive', label: this.tKey('agents.lifecycle.archived') };
}
return { key: 'publish', label: this.tKey('agents.lifecycle.publish') };
},
@@ -662,6 +729,44 @@ document.addEventListener('alpine:init', function() {
return this.tKey('agents.lifecycle.draft');
},
lifecycleDisabled(agent) {
return this.saving
|| agent.raw_status === 'archived'
|| (this.lifecycleBusyId && this.lifecycleBusyId !== agent.id);
},
agentRevisionText(agent) {
return this.tfKey('agents.card.revision', {
draft: agent.current_draft_version || 1,
published: agent.latest_published_version || '—',
revision: agent.catalog_revision || 0,
});
},
drawerRevisionText() {
if (this.drawerMode !== 'edit') return '';
return this.tfKey('agents.drawer.revision', {
draft: this.form.currentDraftVersion || 1,
published: this.form.latestPublishedVersion || '—',
revision: this.form.catalogRevision || 0,
});
},
agentMutationErrorMessage(error, fallback) {
var errorCode = error
&& error.payload
&& error.payload.error
&& error.payload.error.context
&& error.payload.error.context.error_code;
if (errorCode === 'agent_stale_revision' || errorCode === 'agent_precondition_required') {
return this.tKey('agents.toast.stale_message');
}
if (errorCode === 'agent_delete_forbidden') {
return this.tKey('agents.toast.delete_forbidden_message');
}
return error && error.message ? error.message : fallback;
},
mcpEndpoint(agent) {
if (agent.mcp_endpoint) return agent.mcp_endpoint;
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
+324 -52
View File
@@ -3,8 +3,15 @@ var AGENTS = [];
var currentWorkspaceId = null;
var currentAgentId = null;
var activeKeyKind = 'mcp_client';
var selectedScopes = new Set(['read']);
var selectedScopes = new Set(['read', 'write']);
var search = '';
var loadGeneration = 0;
var modalGeneration = 0;
var keyMutations = {};
var onboardingQueryHydrated = false;
var onboardingRevealWasCreated = false;
var createReconciliationRequired = false;
var revealedKeyGeneration = 0;
var SCOPES_BY_KIND = {
mcp_client: ['read', 'write', 'deploy'],
@@ -58,6 +65,7 @@ function mapAgentRecord(record) {
slug: record.slug,
displayName: record.display_name || record.slug || record.id,
mcpEndpoint: record.mcp_endpoint || '',
catalogRevision: Number(record.catalog_revision || 0),
};
}
@@ -67,11 +75,13 @@ function currentWorkspace() {
async function loadKeys() {
var workspace = currentWorkspace();
currentWorkspaceId = workspace ? workspace.id : null;
var workspaceId = workspace ? workspace.id : null;
var generation = ++loadGeneration;
currentWorkspaceId = workspaceId;
setTableLoading(true);
if (!currentWorkspaceId || !window.CrankApi) {
if (!workspaceId || !window.CrankApi) {
AGENTS = [];
KEYS = [];
currentAgentId = null;
@@ -79,11 +89,14 @@ async function loadKeys() {
setCreateButtonState();
setTableLoading(false);
renderTable(tKey('apikeys.error.api'));
return;
return false;
}
try {
var response = await window.CrankApi.listAgents(currentWorkspaceId);
var response = await window.CrankApi.listAgents(workspaceId);
if (generation !== loadGeneration || workspaceId !== (currentWorkspace() && currentWorkspace().id)) {
return;
}
AGENTS = ((response && response.items) || []).map(mapAgentRecord);
if (!AGENTS.length) {
currentAgentId = null;
@@ -92,21 +105,38 @@ async function loadKeys() {
setCreateButtonState();
setTableLoading(false);
renderTable();
return;
return true;
}
if (!currentAgentId || !AGENTS.some(function(agent) { return agent.id === currentAgentId; })) {
var onboardingParams = new URLSearchParams(window.location.search);
var requestedAgentId = onboardingParams.get('onboarding') === '1' ? onboardingParams.get('agentId') : '';
if (requestedAgentId && AGENTS.some(function(agent) { return agent.id === requestedAgentId; })) {
currentAgentId = requestedAgentId;
} else if (!currentAgentId || !AGENTS.some(function(agent) { return agent.id === currentAgentId; })) {
currentAgentId = AGENTS[0].id;
}
var agentId = currentAgentId;
renderAgentPicker();
var keysResponse = await window.CrankApi.listAgentPlatformApiKeys(
currentWorkspaceId,
currentAgentId
workspaceId,
agentId
);
if (
generation !== loadGeneration
|| workspaceId !== (currentWorkspace() && currentWorkspace().id)
|| agentId !== currentAgentId
) {
return;
}
KEYS = ((keysResponse && keysResponse.items) || []).map(mapKeyRecord);
setCreateButtonState();
setTableLoading(false);
renderTable();
hydrateOnboardingKeyQuery();
return true;
} catch (error) {
if (generation !== loadGeneration || workspaceId !== (currentWorkspace() && currentWorkspace().id)) {
return;
}
AGENTS = [];
KEYS = [];
currentAgentId = null;
@@ -114,6 +144,7 @@ async function loadKeys() {
setCreateButtonState();
setTableLoading(false);
renderTable(error.message || tKey('apikeys.error.load'));
return false;
}
}
@@ -133,48 +164,68 @@ function setTableLoading(on) {
async function revokeKey(id) {
if (!confirm(tKey('apikeys.confirm.revoke'))) return;
if (!currentAgentId) return;
if (keyMutations[id]) return;
try {
var workspaceId = currentWorkspaceId;
var agentId = currentAgentId;
var key = KEYS.find(function(item) { return item.id === id; });
await window.CrankApi.revokeAgentPlatformApiKey(currentWorkspaceId, currentAgentId, id);
await loadKeys();
if (window.CrankUi) {
keyMutations[id] = true;
renderTable();
await window.CrankApi.revokeAgentPlatformApiKey(workspaceId, agentId, id);
if (workspaceId === currentWorkspaceId && agentId === currentAgentId) {
await loadKeys();
}
if (window.CrankUi && workspaceId === currentWorkspaceId && agentId === currentAgentId) {
window.CrankUi.success(
tfKey('apikeys.toast.revoke_message', { name: key ? key.name : '' }),
tKey('apikeys.toast.revoke_title')
);
}
} catch (error) {
if (window.CrankUi) {
if (window.CrankUi && workspaceId === currentWorkspaceId && agentId === currentAgentId) {
window.CrankUi.error(
error.message || tKey('apikeys.toast.revoke_error_message'),
tKey('apikeys.toast.revoke_error_title')
);
}
} finally {
delete keyMutations[id];
renderTable();
}
}
async function deleteKey(id) {
if (!confirm(tKey('apikeys.confirm.delete'))) return;
if (!currentAgentId) return;
if (keyMutations[id]) return;
try {
var workspaceId = currentWorkspaceId;
var agentId = currentAgentId;
var key = KEYS.find(function(item) { return item.id === id; });
await window.CrankApi.deleteAgentPlatformApiKey(currentWorkspaceId, currentAgentId, id);
await loadKeys();
if (window.CrankUi) {
keyMutations[id] = true;
renderTable();
await window.CrankApi.deleteAgentPlatformApiKey(workspaceId, agentId, id);
if (workspaceId === currentWorkspaceId && agentId === currentAgentId) {
await loadKeys();
}
if (window.CrankUi && workspaceId === currentWorkspaceId && agentId === currentAgentId) {
window.CrankUi.success(
tfKey('apikeys.toast.delete_message', { name: key ? key.name : '' }),
tKey('apikeys.toast.delete_title')
);
}
} catch (error) {
if (window.CrankUi) {
if (window.CrankUi && workspaceId === currentWorkspaceId && agentId === currentAgentId) {
window.CrankUi.error(
error.message || tKey('apikeys.toast.delete_error_message'),
tKey('apikeys.toast.delete_error_title')
);
}
} finally {
delete keyMutations[id];
renderTable();
}
}
@@ -187,9 +238,84 @@ async function createKey(name, scopes) {
return {
rawKey: created.secret,
record: mapKeyRecord(created.api_key),
connection: created.connection || null,
};
}
function isOnboardingKeyQuery() {
var params = new URLSearchParams(window.location.search);
return params.get('onboarding') === '1' && params.get('action') === 'create';
}
function hydrateOnboardingKeyQuery() {
if (!isOnboardingKeyQuery() || onboardingQueryHydrated) return;
onboardingQueryHydrated = true;
var params = new URLSearchParams(window.location.search);
var selected = currentAgent();
var expectedRevision = Number(params.get('agentRevision') || 0);
if (!selected || (expectedRevision && selected.catalogRevision !== expectedRevision)) return;
var hasActiveKey = KEYS.some(function(key) { return key.status === 'active' && key.keyKind === 'mcp_client'; });
if (hasActiveKey) {
showLostKeyRecovery();
return;
}
activeKeyKind = 'mcp_client';
renderKeyKindTabs();
openModal();
}
function showLostKeyRecovery() {
var element = document.getElementById('onboarding-key-lost');
if (element) element.hidden = false;
}
function renderEphemeralConnection(connection) {
var root = document.getElementById('onboarding-connection-config');
var clients = document.getElementById('onboarding-connection-clients');
var warning = document.getElementById('onboarding-clipboard-warning');
if (!root || !clients || !warning) return;
clients.replaceChildren();
if (!connection || !connection.endpoint) {
root.hidden = true;
warning.hidden = true;
return;
}
var endpoint = document.createElement('code');
endpoint.textContent = connection.endpoint;
clients.appendChild(endpoint);
(connection.clients || []).forEach(function(client) {
var block = document.createElement('div');
block.style.marginTop = '10px';
var label = document.createElement('strong');
label.textContent = client.client;
var pre = document.createElement('pre');
pre.className = 'log-detail-block';
pre.textContent = JSON.stringify(client.config, null, 2);
var copy = document.createElement('button');
copy.type = 'button';
copy.className = 'btn-secondary';
copy.textContent = tKey('apikeys.modal.copy_config');
copy.addEventListener('click', async function() {
var value = pre.textContent;
copy.disabled = true;
try {
await copyEphemeralValue(value);
wipeRevealedKey();
} catch (_error) {
showClipboardFailure();
} finally {
copy.disabled = false;
}
});
block.appendChild(label);
block.appendChild(pre);
block.appendChild(copy);
clients.appendChild(block);
});
root.hidden = false;
warning.hidden = false;
}
function currentAgent() {
return AGENTS.find(function(agent) { return agent.id === currentAgentId; }) || null;
}
@@ -311,7 +437,7 @@ function renderTable(errorMessage) {
if (subtitle) {
var active = visibleKeys.filter(function(key) { return key.status === 'active'; }).length;
var revoked = visibleKeys.filter(function(key) { return key.status === 'revoked'; }).length;
var revoked = visibleKeys.filter(function(key) { return key.status !== 'active'; }).length;
subtitle.textContent = currentAgentId
? tfKey('apikeys.active.subtitle', { active: active, revoked: revoked })
: tKey('apikeys.agent.empty_hint');
@@ -325,7 +451,7 @@ function renderTable(errorMessage) {
errorCell.textContent = errorMessage;
errorRow.appendChild(errorCell);
tbody.appendChild(errorRow);
renderKeyCards([], errorMessage);
renderKeyCards([], errorMessage, visibleKeys.length);
return;
}
@@ -339,7 +465,7 @@ function renderTable(errorMessage) {
: tKey('apikeys.agent.empty_hint');
empty.appendChild(td);
tbody.appendChild(empty);
renderKeyCards(rows);
renderKeyCards(rows, null, visibleKeys.length);
return;
}
@@ -361,33 +487,42 @@ function renderTable(errorMessage) {
var badge = document.createElement('span');
badge.className = key.status === 'active' ? 'badge badge-active' : 'badge badge-revoked';
badge.textContent = key.status === 'active' ? tKey('apikeys.status.active') : tKey('apikeys.status.revoked');
badge.textContent = keyStatusLabel(key.status);
node.querySelector('.col-status').appendChild(badge);
var actionsActive = node.querySelector('.actions-active');
var actionsRevoked = node.querySelector('.actions-revoked');
if (key.status === 'active') {
actionsActive.querySelectorAll('button').forEach(function(button) {
button.disabled = Boolean(keyMutations[key.id]);
});
actionsActive.querySelector('[title=\"Copy key prefix\"]').addEventListener('click', function() {
copyPrefix(key.prefix);
});
actionsActive.querySelector('[title=\"Revoke key\"]').addEventListener('click', function() {
revokeKey(key.id);
});
} else {
} else if (key.status === 'revoked') {
actionsActive.hidden = true;
actionsRevoked.hidden = false;
actionsRevoked.querySelectorAll('button').forEach(function(button) {
button.disabled = Boolean(keyMutations[key.id]);
});
actionsRevoked.querySelector('[title=\"Delete\"]').addEventListener('click', function() {
deleteKey(key.id);
});
} else {
actionsActive.hidden = true;
actionsRevoked.hidden = true;
}
tbody.appendChild(node);
});
renderKeyCards(rows);
renderKeyCards(rows, null, visibleKeys.length);
}
function renderKeyCards(rows, errorMessage) {
function renderKeyCards(rows, errorMessage, visibleKeyCount) {
var cardList = document.getElementById('keys-card-list');
if (!cardList) return;
@@ -402,7 +537,7 @@ function renderKeyCards(rows, errorMessage) {
cardList.appendChild(
buildKeyCardMessage(
currentAgentId
? (visibleKeys.length ? tKey('apikeys.empty.search') : emptyTextForKind())
? (visibleKeyCount ? tKey('apikeys.empty.search') : emptyTextForKind())
: tKey('apikeys.agent.empty_hint'),
false
)
@@ -431,9 +566,7 @@ function renderKeyCards(rows, errorMessage) {
var statusBadge = document.createElement('span');
statusBadge.className = key.status === 'active' ? 'badge badge-active' : 'badge badge-revoked';
statusBadge.textContent = key.status === 'active'
? tKey('apikeys.status.active')
: tKey('apikeys.status.revoked');
statusBadge.textContent = keyStatusLabel(key.status);
actions.appendChild(statusBadge);
header.appendChild(headerMain);
header.appendChild(actions);
@@ -451,14 +584,14 @@ function renderKeyCards(rows, errorMessage) {
if (key.status === 'active') {
actionRow.appendChild(buildCardAction('apikeys.action.copy_prefix', function() {
copyPrefix(key.prefix);
}));
}, false, Boolean(keyMutations[key.id])));
actionRow.appendChild(buildCardAction('apikeys.action.revoke', function() {
revokeKey(key.id);
}, true));
} else {
}, true, Boolean(keyMutations[key.id])));
} else if (key.status === 'revoked') {
actionRow.appendChild(buildCardAction('apikeys.action.delete', function() {
deleteKey(key.id);
}, true));
}, true, Boolean(keyMutations[key.id])));
}
card.appendChild(actionRow);
@@ -477,6 +610,12 @@ function buildKeyCardMessage(text, isError) {
return card;
}
function keyStatusLabel(status) {
if (status === 'active') return tKey('apikeys.status.active');
if (status === 'deleted') return tKey('apikeys.status.deleted');
return tKey('apikeys.status.revoked');
}
function emptyTextForKind() {
return activeKeyKind === 'approval'
? tKey('apikeys.empty.approval')
@@ -497,25 +636,70 @@ function buildMetaItem(labelKey, valueText) {
return item;
}
function buildCardAction(labelKey, onClick, isDanger) {
function buildCardAction(labelKey, onClick, isDanger, disabled) {
var button = document.createElement('button');
button.type = 'button';
button.className = isDanger ? 'btn-secondary' : 'btn-secondary';
button.textContent = tKey(labelKey);
button.disabled = Boolean(disabled);
button.addEventListener('click', onClick);
return button;
}
var modal = document.getElementById('modal-create');
function wipeRevealedKey(expectedGeneration) {
if (expectedGeneration && revealedKeyGeneration && revealedKeyGeneration !== expectedGeneration) {
return;
}
var keyValue = document.getElementById('reveal-key-value');
if (keyValue) {
keyValue.textContent = '';
}
var clients = document.getElementById('onboarding-connection-clients');
if (clients) clients.replaceChildren();
var connection = document.getElementById('onboarding-connection-config');
if (connection) connection.hidden = true;
var warning = document.getElementById('onboarding-clipboard-warning');
if (warning) warning.hidden = true;
var copyButton = document.getElementById('copy-key-btn');
if (copyButton) {
copyButton.replaceChildren(
buildIconSvg(
(window.APP_BASE || '') + 'icons/general/copy.svg#icon',
14,
14
)
);
}
revealedKeyGeneration = 0;
}
function resetCreateButton() {
var button = document.getElementById('modal-confirm-btn');
if (!button) return;
button.disabled = !currentAgentId;
button.textContent = tKey('apikeys.modal.create');
}
function invalidateModalState() {
modalGeneration += 1;
wipeRevealedKey();
resetCreateButton();
createReconciliationRequired = false;
var warning = document.getElementById('ambiguous-create-warning');
if (warning) warning.hidden = true;
}
function openModal() {
if (!currentAgentId) return;
invalidateModalState();
document.getElementById('modal-form-body').hidden = false;
document.getElementById('modal-reveal-body').hidden = true;
document.getElementById('modal-footer-create').hidden = false;
document.getElementById('modal-footer-done').hidden = true;
document.getElementById('new-key-name').value = '';
selectedScopes = new Set([activeKeyKind === 'approval' ? 'approve' : 'read']);
selectedScopes = new Set(activeKeyKind === 'approval' ? ['approve'] : ['read', 'write']);
document.getElementById('modal-create-title').textContent = activeKeyKind === 'approval'
? tKey('apikeys.modal.title_approval')
: tKey('apikeys.modal.title_mcp');
@@ -529,6 +713,7 @@ function openModal() {
function closeModal() {
modal.classList.remove('open');
invalidateModalState();
}
document.getElementById('btn-create-key').addEventListener('click', openModal);
@@ -558,14 +743,34 @@ document.getElementById('modal-confirm-btn').addEventListener('click', async fun
try {
button.disabled = true;
button.textContent = tKey('apikeys.creating');
var requestGeneration = ++modalGeneration;
var workspaceId = currentWorkspaceId;
var agentId = currentAgentId;
var keyKind = activeKeyKind;
var created = await createKey(name, Array.from(selectedScopes));
if (
requestGeneration !== modalGeneration
|| workspaceId !== currentWorkspaceId
|| agentId !== currentAgentId
|| keyKind !== activeKeyKind
|| !modal.classList.contains('open')
) {
wipeRevealedKey(requestGeneration);
var reconciled = await loadKeys();
if (reconciled) showLostKeyRecovery();
return;
}
KEYS.unshift(created.record);
document.getElementById('reveal-key-value').textContent = created.rawKey;
revealedKeyGeneration = requestGeneration;
onboardingRevealWasCreated = true;
renderEphemeralConnection(keyKind === 'mcp_client' ? created.connection : null);
document.getElementById('modal-form-body').hidden = true;
document.getElementById('modal-reveal-body').hidden = false;
document.getElementById('modal-footer-create').hidden = true;
document.getElementById('modal-footer-done').hidden = false;
renderTable();
if (window.CrankOnboarding) window.CrankOnboarding.signalRefresh();
if (window.CrankUi) {
window.CrankUi.success(
tKey('apikeys.toast.create_message'),
@@ -573,38 +778,87 @@ document.getElementById('modal-confirm-btn').addEventListener('click', async fun
);
}
} catch (error) {
var ambiguous = !error.status || error.status === 408 || error.status >= 500;
if (ambiguous
&& workspaceId === currentWorkspaceId
&& agentId === currentAgentId
&& modal.classList.contains('open')) {
createReconciliationRequired = true;
var warning = document.getElementById('ambiguous-create-warning');
if (warning) warning.hidden = false;
await loadKeys();
}
if (window.CrankUi) {
window.CrankUi.error(
error.message || tKey('apikeys.toast.create_error_message'),
tKey('apikeys.toast.create_error_title')
ambiguous ? tKey('apikeys.ambiguous.body') : (error.message || tKey('apikeys.toast.create_error_message')),
ambiguous ? tKey('apikeys.ambiguous.title') : tKey('apikeys.toast.create_error_title')
);
}
} finally {
button.disabled = false;
button.textContent = tKey('apikeys.modal.create');
if (requestGeneration === modalGeneration && button && modal.classList.contains('open')) {
button.disabled = createReconciliationRequired || !currentAgentId;
button.textContent = createReconciliationRequired
? tKey('apikeys.ambiguous.blocked')
: tKey('apikeys.modal.create');
}
}
});
document.getElementById('modal-done-btn').addEventListener('click', closeModal);
document.getElementById('copy-key-btn').addEventListener('click', function() {
var value = document.getElementById('reveal-key-value').textContent;
if (navigator.clipboard) {
navigator.clipboard.writeText(value).catch(function() {});
function copyEphemeralValue(value) {
if (!value || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
return Promise.reject(new Error('clipboard unavailable'));
}
this.replaceChildren(
buildIconSvg(
(window.APP_BASE || '') + 'icons/general/check.svg#icon',
13,
13
)
);
return navigator.clipboard.writeText(value);
}
function showClipboardFailure() {
if (window.CrankUi) {
window.CrankUi.info(
tKey('apikeys.toast.copy_message'),
tKey('apikeys.toast.copy_title')
window.CrankUi.error(
tKey('apikeys.toast.copy_error_message'),
tKey('apikeys.toast.copy_error_title')
);
}
}
document.getElementById('copy-key-btn').addEventListener('click', async function() {
var value = document.getElementById('reveal-key-value').textContent;
this.disabled = true;
try {
await copyEphemeralValue(value);
this.replaceChildren(
buildIconSvg(
(window.APP_BASE || '') + 'icons/general/check.svg#icon',
13,
13
)
);
if (window.CrankUi) {
window.CrankUi.info(
tKey('apikeys.toast.copy_message'),
tKey('apikeys.toast.copy_title')
);
}
wipeRevealedKey();
} catch (_error) {
showClipboardFailure();
} finally {
this.disabled = false;
}
});
document.getElementById('ambiguous-create-retry-btn').addEventListener('click', async function() {
var button = this;
button.disabled = true;
var reconciled = await loadKeys();
if (reconciled && currentAgentId && modal.classList.contains('open')) {
createReconciliationRequired = false;
document.getElementById('ambiguous-create-warning').hidden = true;
resetCreateButton();
document.getElementById('new-key-name').focus();
}
button.disabled = false;
});
document.getElementById('key-search').addEventListener('input', function() {
@@ -613,12 +867,14 @@ document.getElementById('key-search').addEventListener('input', function() {
});
document.getElementById('agent-select').addEventListener('change', async function() {
closeModal();
currentAgentId = this.value || null;
await loadKeys();
});
document.querySelectorAll('[data-key-kind]').forEach(function(button) {
button.addEventListener('click', function() {
closeModal();
activeKeyKind = this.dataset.keyKind || 'mcp_client';
search = '';
document.getElementById('key-search').value = '';
@@ -641,6 +897,22 @@ document.addEventListener('DOMContentLoaded', async function() {
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
await loadKeys();
window.addEventListener('crank:workspacechange', function() {
keyMutations = {};
closeModal();
onboardingQueryHydrated = false;
loadKeys();
});
window.addEventListener('crank:langchange', function() {
closeModal();
if (onboardingRevealWasCreated && isOnboardingKeyQuery()) showLostKeyRecovery();
});
window.addEventListener('pagehide', function() {
closeModal();
});
window.addEventListener('pageshow', function(event) {
if (event.persisted) {
closeModal();
if (onboardingRevealWasCreated && isOnboardingKeyQuery()) showLostKeyRecovery();
}
});
});
+188 -5
View File
@@ -1,6 +1,8 @@
(function() {
var API_BASE = '/api/admin';
var AUTH_BASE = '/api/auth';
var operationEtags = Object.create(null);
var agentEtags = Object.create(null);
function headers(extra) {
return Object.assign({
@@ -8,6 +10,26 @@
}, extra || {});
}
function csrfExempt(path) {
return /\/api\/auth\/(?:login|bootstrap\/complete|session\/csrf)$/.test(path);
}
function attachCsrf(path, method, requestOptions) {
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS' || csrfExempt(path)) {
return;
}
if (!(path.indexOf('/api/auth/') === 0 || path.indexOf('/api/admin/') === 0)) {
return;
}
var token = window.CrankAuth && typeof window.CrankAuth.getCsrfToken === 'function'
? window.CrankAuth.getCsrfToken()
: '';
if (token) {
requestOptions.headers = headers(requestOptions.headers);
requestOptions.headers['x-csrf-token'] = token;
}
}
function attachCorrelation(error, response) {
var requestId = response.headers.get('x-request-id');
var traceId = response.headers.get('x-trace-id');
@@ -20,13 +42,88 @@
return error;
}
function operationMutationResource(path, method) {
if (!method || method === 'GET') {
return null;
}
var match = path.match(/^(\/api\/admin\/workspaces\/[^/]+\/operations\/[^/?]+)(?:\/(publish|archive|versions))?(?:\?.*)?$/);
if (!match) {
return null;
}
if (method === 'PATCH' || method === 'DELETE' || (method === 'POST' && match[2])) {
return match[1];
}
return null;
}
function operationDetailResource(path, method) {
if (method && method !== 'GET') {
return null;
}
var match = path.match(/^(\/api\/admin\/workspaces\/[^/]+\/operations\/[^/?]+)$/);
return match ? match[1] : null;
}
function agentMutationResource(path, method) {
if (!method || method === 'GET') {
return null;
}
var match = path.match(/^(\/api\/admin\/workspaces\/[^/]+\/agents\/[^/?]+)(?:\/(bindings|publish|unpublish|archive))?(?:\?.*)?$/);
if (!match) {
return null;
}
if (method === 'PATCH' || method === 'DELETE' || (method === 'POST' && match[2])) {
return match[1];
}
return null;
}
function agentDetailResource(path, method) {
if (method && method !== 'GET') {
return null;
}
var match = path.match(/^(\/api\/admin\/workspaces\/[^/]+\/agents\/[^/?]+)$/);
return match ? match[1] : null;
}
async function request(path, options) {
var response = await fetch(path, Object.assign({
var requestOptions = Object.assign({
credentials: 'same-origin',
headers: headers(),
}, options || {}));
}, options || {});
var method = (requestOptions.method || 'GET').toUpperCase();
attachCsrf(path, method, requestOptions);
var mutationResource = operationMutationResource(path, method);
var agentMutation = agentMutationResource(path, method);
if (mutationResource) {
if (!operationEtags[mutationResource]) {
await request(mutationResource);
}
requestOptions.headers = headers(requestOptions.headers);
requestOptions.headers['If-Match'] = operationEtags[mutationResource];
}
if (agentMutation) {
if (!agentEtags[agentMutation]) {
await request(agentMutation);
}
requestOptions.headers = headers(requestOptions.headers);
requestOptions.headers['If-Match'] = agentEtags[agentMutation];
}
var response = await fetch(path, requestOptions);
var detailResource = operationDetailResource(path, method);
var agentDetail = agentDetailResource(path, method);
var responseEtag = response.headers.get('etag');
if (detailResource && responseEtag) {
operationEtags[detailResource] = responseEtag;
}
if (agentDetail && responseEtag) {
agentEtags[agentDetail] = responseEtag;
}
if (response.status === 204) {
if (mutationResource) {
delete operationEtags[mutationResource];
}
return null;
}
@@ -40,6 +137,12 @@
}
if (!response.ok) {
if (mutationResource && (response.status === 409 || response.status === 428)) {
delete operationEtags[mutationResource];
}
if (agentMutation && (response.status === 409 || response.status === 428)) {
delete agentEtags[agentMutation];
}
if (response.status === 401 && window.CrankAuth && typeof window.CrankAuth.handleUnauthorized === 'function') {
window.CrankAuth.handleUnauthorized();
}
@@ -54,9 +157,22 @@
var error = new Error(message);
error.status = response.status;
error.payload = payload;
var errorCode = payload && payload.error && typeof payload.error === 'object'
? (payload.error.code || payload.error.error_code)
: payload && (payload.code || payload.error_code);
if (typeof errorCode === 'string' && /^[A-Za-z0-9._-]{1,128}$/.test(errorCode)) {
error.code = errorCode;
}
throw attachCorrelation(error, response);
}
if (mutationResource) {
delete operationEtags[mutationResource];
}
if (agentMutation) {
delete agentEtags[agentMutation];
}
if (text && payload === null) {
throw attachCorrelation(new Error('Backend returned a non-JSON response'), response);
}
@@ -137,6 +253,16 @@
}
window.CrankApi = {
getBootstrapStatus: function() {
return request(AUTH_BASE + '/bootstrap/status');
},
completeBootstrap: function(payload) {
return request(AUTH_BASE + '/bootstrap/complete', {
method: 'POST',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload),
});
},
login: function(payload) {
return request(AUTH_BASE + '/login', {
method: 'POST',
@@ -154,6 +280,13 @@
getSession: function() {
return request(AUTH_BASE + '/session');
},
refreshSessionCsrf: function() {
return request(AUTH_BASE + '/session/csrf', {
method: 'POST',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify({}),
});
},
getProfile: function() {
return request(AUTH_BASE + '/profile');
},
@@ -180,6 +313,21 @@
getWorkspace: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId));
},
getOnboarding: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/onboarding');
},
recordOnboardingEvent: function(workspaceId, payload, options) {
return request(API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/onboarding/events', Object.assign({}, options || {}, {
method: 'POST',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload),
}));
},
resetOnboardingSelection: function(workspaceId, expectedRevision) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/onboarding/reset-selection', {
expected_revision: expectedRevision,
});
},
updateWorkspace: function(workspaceId, payload) {
return patch('/workspaces/' + encodeURIComponent(workspaceId), payload);
},
@@ -238,15 +386,28 @@
}
);
},
importOperation: function(workspaceId, yamlDocument, mode) {
return request(
importOperation: async function(workspaceId, yamlDocument, mode, existingOperationId) {
var importHeaders = headers({ 'Content-Type': 'application/yaml' });
if (existingOperationId) {
var resource = API_BASE + '/workspaces/' + encodeURIComponent(workspaceId)
+ '/operations/' + encodeURIComponent(existingOperationId);
if (!operationEtags[resource]) {
await request(resource);
}
importHeaders['If-Match'] = operationEtags[resource];
}
var result = await request(
API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/operations/import' + query({ mode: mode }),
{
method: 'POST',
headers: headers({ 'Content-Type': 'application/yaml' }),
headers: importHeaders,
body: yamlDocument,
}
);
if (existingOperationId) {
delete operationEtags[resource];
}
return result;
},
previewOpenApiImport: function(workspaceId, documentText) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/imports/openapi/preview', {
@@ -331,6 +492,16 @@
listLogs: function(workspaceId, params) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs' + query(params));
},
exportLogsCsv: function(workspaceId, params) {
return requestText(
API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/logs/export.csv' + query(params)
);
},
exportUsageCsv: function(workspaceId, params) {
return requestText(
API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/usage/export.csv' + query(params)
);
},
getLog: function(workspaceId, logId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs/' + encodeURIComponent(logId));
},
@@ -340,6 +511,18 @@
getApproval: function(workspaceId, approvalId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/approvals/' + encodeURIComponent(approvalId));
},
approveApproval: function(workspaceId, approvalId, payload) {
return post(
'/workspaces/' + encodeURIComponent(workspaceId) + '/approvals/' + encodeURIComponent(approvalId) + '/approve',
payload || { approve: 'yes' }
);
},
denyApproval: function(workspaceId, approvalId, payload) {
return post(
'/workspaces/' + encodeURIComponent(workspaceId) + '/approvals/' + encodeURIComponent(approvalId) + '/deny',
payload || { approve: 'no' }
);
},
getUsageOverview: function(workspaceId, params) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/usage' + query(params));
},
+22
View File
@@ -175,6 +175,15 @@
}
sessionPromise = window.CrankApi.getSession()
.then(function(session) {
if (session && !session.csrf_token && window.CrankApi && typeof window.CrankApi.refreshSessionCsrf === 'function') {
return window.CrankApi.refreshSessionCsrf().then(function(csrf) {
session.csrf_token = csrf && csrf.csrf_token ? csrf.csrf_token : '';
return session;
});
}
return session;
})
.then(function(session) {
return replaceSession(session);
})
@@ -221,6 +230,15 @@
window.location.href = homeUrl();
}
async function completeBootstrap(token, password) {
var session = await window.CrankApi.completeBootstrap({
token: token,
password: password,
});
replaceSession(session);
window.location.href = homeUrl();
}
async function logout() {
try {
await window.CrankApi.logout();
@@ -243,12 +261,16 @@
fetchSession: fetchSession,
replaceSession: replaceSession,
getCachedSession: function() { return sessionCache; },
getCsrfToken: function() {
return sessionCache && sessionCache.csrf_token ? sessionCache.csrf_token : '';
},
renderShellIdentity: function() {
renderShellIdentity(sessionCache);
},
guardProtectedPage: guardProtectedPage,
guardLoginPage: guardLoginPage,
login: login,
completeBootstrap: completeBootstrap,
logout: logout,
handleUnauthorized: handleUnauthorized,
};
+36 -3
View File
@@ -44,6 +44,9 @@ function mapOperation(item) {
created_at: item.created_at,
updated_at: item.updated_at,
published_at: item.published_at,
current_draft_version: item.current_draft_version,
latest_published_version: item.latest_published_version,
can_delete: item.can_delete === true,
target_url: item.target_url || '',
method: item.target_action || '',
usage_summary: item.usage_summary || {
@@ -117,6 +120,7 @@ document.addEventListener('alpine:init', function() {
_agentsByOpIdCacheVersion: -1,
_activeAgentsCache: null,
_activeAgentsCacheVersion: -1,
_loadGeneration: 0,
async init() {
var self = this;
@@ -153,25 +157,31 @@ document.addEventListener('alpine:init', function() {
},
async reload() {
var generation = ++this._loadGeneration;
var requestedWorkspaceId = this.workspaceId;
this.loading = true;
this.loadError = '';
try {
var response = await window.CrankApi.listOperations(this.workspaceId);
var response = await window.CrankApi.listOperations(requestedWorkspaceId);
if (generation !== this._loadGeneration || requestedWorkspaceId !== this.workspaceId) return;
this.replaceOperations((response && response.items ? response.items : []).map(mapOperation));
this.categoryOptions = Array.from(new Set(this.operations.map(function(operation) {
return operation.category;
}).filter(Boolean))).sort();
this.stats = computeStats(this.operations);
} catch (error) {
if (generation !== this._loadGeneration || requestedWorkspaceId !== this.workspaceId) return;
this.replaceOperations([]);
this.categoryOptions = [];
this.stats = emptyStats();
this.loadError = error.message || 'Failed to load operations';
}
this.loading = false;
this.page = 1;
if (generation === this._loadGeneration && requestedWorkspaceId === this.workspaceId) {
this.loading = false;
this.page = 1;
}
},
replaceOperations(operations) {
@@ -453,6 +463,29 @@ document.addEventListener('alpine:init', function() {
}
},
async archiveOperation(operation) {
if (!operation || !confirm(this.tKey('ops.archive.confirm'))) return;
try {
await window.CrankApi.archiveOperation(this.workspaceId, operation.id);
await this.reload();
if (window.CrankUi) {
window.CrankUi.success(
this.tfKey('ops.archive.success.message', {
name: operation.display_name || operation.name
}),
this.tKey('ops.archive.success.title')
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || this.tKey('ops.archive.error.message'),
this.tKey('ops.archive.error.title')
);
}
}
},
protocolLabel(operation) {
return operation.method ? ('REST · ' + operation.method) : 'REST';
},
+312 -2
View File
@@ -20,6 +20,43 @@ var TRANSLATIONS = {
'nav.user_fallback': 'Crank',
'nav.workspace_fallback': 'workspace',
'onboarding.title': 'Getting Started',
'onboarding.subtitle': 'A resumable path to your first real MCP tool call.',
'onboarding.loading': 'Checking authoritative progress…',
'onboarding.progress': '{count}/7 steps complete',
'onboarding.error': 'Getting Started progress is unavailable.',
'onboarding.retry': 'Retry',
'onboarding.refresh': 'Refresh progress',
'onboarding.dismiss': 'Dismiss',
'onboarding.resume': 'Resume Getting Started',
'onboarding.collapse': 'Collapse Getting Started checklist',
'onboarding.request_id': 'Request ID',
'onboarding.trace_id': 'Trace ID',
'onboarding.error_code': 'Error code',
'onboarding.progress_label': 'Getting Started progress',
'onboarding.action_needed': 'This is the next actionable step.',
'onboarding.regressed': 'The referenced object changed. Review and continue again.',
'onboarding.completed': 'Your first public MCP tool call is complete.',
'onboarding.open_invocation': 'Open invocation history',
'onboarding.tool': 'Tool',
'onboarding.timestamp': 'Timestamp',
'onboarding.deep_link_stale': 'This onboarding link references a changed Operation or Agent. Reset it and reselect the current published revision.',
'onboarding.reselect': 'Reset and reselect',
'onboarding.step.operation': 'Create an Operation',
'onboarding.step.test': 'Test the Operation',
'onboarding.step.publish_operation': 'Publish the Operation',
'onboarding.step.agent': 'Create and publish an Agent',
'onboarding.step.key': 'Create an MCP client key',
'onboarding.step.mcp_connection': 'Connect an MCP client',
'onboarding.step.first_call': 'Make the first tool call',
'onboarding.action.operation': 'Create Operation',
'onboarding.action.test': 'Open test',
'onboarding.action.publish_operation': 'Open publication',
'onboarding.action.agent': 'Create Agent',
'onboarding.action.key': 'Create key',
'onboarding.action.mcp_connection': 'Show connection',
'onboarding.action.first_call': 'Open Logs',
// Operations page
'ops.title': 'Operations',
'ops.subtitle': 'Catalog of tools. List of created MCP tools for API endpoints.',
@@ -37,6 +74,12 @@ var TRANSLATIONS = {
'ops.delete.error.message': 'Failed to delete operation',
'ops.action.edit': 'Edit operation',
'ops.action.delete':'Delete operation',
'ops.action.archive': 'Archive operation',
'ops.archive.confirm': 'Archive this operation? Existing published Agent snapshots remain available, but new publications and bindings will be blocked.',
'ops.archive.success.title': 'Operation archived',
'ops.archive.success.message': '{name} was archived.',
'ops.archive.error.title': 'Archive failed',
'ops.archive.error.message': 'Failed to archive operation',
'ops.stats.synced': 'Catalog synced with backend',
'ops.stats.none': 'No operations yet',
'ops.stats.share': '{value}% of total',
@@ -137,11 +180,16 @@ var TRANSLATIONS = {
'apikeys.modal.reveal_title': 'Copy this key now.',
'apikeys.modal.reveal_body': "It won't be shown again.",
'apikeys.modal.copy': 'Copy to clipboard',
'apikeys.modal.copy_config': 'Copy configuration',
'apikeys.modal.connection_title': 'MCP connection configuration',
'apikeys.modal.clipboard_warning': 'Your operating system clipboard is outside Crank control. Clear it after saving the key.',
'apikeys.onboarding.key_lost': 'The key cannot be shown again. Create a new key and revoke the old one if its value was not saved.',
'apikeys.modal.cancel': 'Cancel',
'apikeys.modal.create': 'Create key',
'apikeys.modal.done': "Done — I've copied the key",
'apikeys.status.active': 'Active',
'apikeys.status.revoked': 'Revoked',
'apikeys.status.deleted': 'Deleted',
'apikeys.last_used.never': 'Never',
'apikeys.empty.none': 'No API keys yet',
'apikeys.empty.approval': 'No approval keys yet. Create one only if this agent has tools that require human confirmation.',
@@ -167,12 +215,18 @@ var TRANSLATIONS = {
'apikeys.toast.create_error_message': 'Failed to create key',
'apikeys.toast.copy_title': 'Agent key copied',
'apikeys.toast.copy_message': 'Store the raw key securely. It cannot be revealed again.',
'apikeys.toast.copy_error_title': 'Clipboard write failed',
'apikeys.toast.copy_error_message': 'The one-time value is still visible. Copy it manually before closing this page.', // community-scope: allow=one-time-token
'apikeys.toast.prefix_title': 'Key prefix copied',
'apikeys.action.copy_prefix': 'Copy key prefix',
'apikeys.action.revoke': 'Revoke key',
'apikeys.action.delete': 'Delete',
'apikeys.creating': 'Creating…',
'apikeys.approval.warning': 'Do not pass this key to an LLM or MCP client. It is only for an external interface where a human confirms an action.',
'apikeys.ambiguous.title': 'Creation result is uncertain.',
'apikeys.ambiguous.body': 'Key metadata was refreshed. Review the list before deliberately creating another key.',
'apikeys.ambiguous.retry': 'Refresh metadata and allow another attempt',
'apikeys.ambiguous.blocked': 'Review metadata before retrying',
// Secrets page
'secrets.title': 'Secrets',
@@ -256,6 +310,16 @@ var TRANSLATIONS = {
'logs.live': 'Live',
'logs.paused': 'Paused',
'logs.refresh': 'Refresh',
'logs.export': 'Export CSV',
'logs.load_more': 'Load more',
'logs.status_filter': 'Status',
'logs.status.all': 'All statuses',
'logs.status.ok': 'Success',
'logs.status.error': 'Error',
'logs.outcome_filter': 'Outcome',
'logs.outcome.all': 'All outcomes',
'logs.filter.operation': 'Operation ID',
'logs.filter.agent': 'Agent ID',
'logs.range.30m': 'Last 30 min',
'logs.range.1h': 'Last hour',
'logs.range.6h': 'Last 6 hours',
@@ -281,6 +345,10 @@ var TRANSLATIONS = {
'logs.live.off.body': 'Automatic polling is paused.',
'logs.refresh.title': 'Logs refreshed',
'logs.refresh.body': 'The latest invocation records were loaded for the current workspace.',
'logs.export.done_title': 'Logs exported',
'logs.export.done_body': 'The filtered invocation history was exported as CSV.',
'logs.export.error_title': 'Export failed',
'logs.export.error_body': 'Failed to export invocation history.',
'approvals.title': 'Human confirmations',
'approvals.subtitle': 'Requests waiting for an external user decision and recent results.',
'approvals.refresh': 'Refresh',
@@ -289,6 +357,7 @@ var TRANSLATIONS = {
'approvals.loading': 'Loading confirmation requests…',
'approvals.empty': 'There are no confirmation requests yet.',
'approvals.error.load': 'Failed to load confirmation requests',
'approvals.error.decision': 'Failed to submit confirmation decision',
'approvals.untitled': 'Confirmation request',
'approvals.operation': 'Operation',
'approvals.agent': 'Agent',
@@ -296,6 +365,13 @@ var TRANSLATIONS = {
'approvals.updated_at': 'Updated',
'approvals.request': 'Request',
'approvals.response': 'Result',
'approvals.request_id': 'Request ID',
'approvals.trace_id': 'Trace ID',
'approvals.action.approve': 'Approve',
'approvals.action.deny': 'Deny',
'approvals.action.busy': 'Submitting…',
'approvals.confirm.approve': 'Approve this pending confirmation request?',
'approvals.confirm.deny': 'Deny this pending confirmation request?',
'approvals.status.pending': 'Pending',
'approvals.status.approved': 'Approved',
'approvals.status.denied': 'Denied',
@@ -328,6 +404,19 @@ var TRANSLATIONS = {
'usage.chart.error': 'Error',
'usage.chart.empty.title': 'No usage data yet',
'usage.chart.empty.sub': 'Invocation metrics will appear here after tests or published tool calls in the selected period.',
'usage.outcomes.title': 'Outcomes',
'usage.outcomes.subtitle': 'Grouped by safe execution outcome.',
'usage.outcomes.empty.title': 'No outcome breakdown yet',
'usage.outcomes.empty.sub': 'Outcome groups will appear after invocations are recorded.',
'usage.outcomes.p50': 'p50',
'usage.outcomes.p95': 'p95',
'usage.outcomes.p99': 'p99',
'usage.outcome.success': 'Success',
'usage.outcome.upstream': 'Upstream',
'usage.outcome.client': 'Client',
'usage.outcome.schema': 'Schema',
'usage.outcome.crank': 'Crank',
'usage.outcome.no_error_code': 'No error code',
'usage.table.title': 'By operation',
'usage.table.subtitle': 'Breakdown for {period}',
'usage.table.th.operation': 'Operation',
@@ -349,7 +438,9 @@ var TRANSLATIONS = {
'usage.export.empty.title': 'No usage data loaded',
'usage.export.empty.body': 'Load usage data before exporting the CSV snapshot.',
'usage.export.done.title': 'Usage exported',
'usage.export.done.body': 'The current usage snapshot was exported as CSV.',
'usage.export.done.body': 'The filtered usage dataset was exported as CSV.',
'usage.export.error.title': 'Export failed',
'usage.export.error.body': 'Failed to export usage data.',
'usage.chart.ok': '{count} ok',
'usage.chart.errors': '{count} errors',
'usage.chart.week': 'Wk {index}',
@@ -403,6 +494,7 @@ var TRANSLATIONS = {
'settings.security.change_password': 'Change password',
'settings.security.mismatch': 'New password and confirmation do not match.',
'settings.security.saved': 'Password updated.',
'settings.security.saved_relogin': 'Password updated. Please sign in again.',
'settings.security.save_error': 'Failed to change password',
'settings.capability.rest': 'REST / HTTP',
'settings.capability.standard': 'standard',
@@ -706,6 +798,36 @@ var TRANSLATIONS = {
'wizard.test.failed': 'Operation test returned errors',
'wizard.test.completed_body': 'The operation was executed successfully against the API.',
'wizard.test.failed_body': 'The operation was executed, but the API or validation returned errors.',
'execution.error.authorization_denied': 'Operation execution is denied.',
'execution.error.auth_profile_not_found': 'Authorization profile was not found.',
'execution.error.secret_not_found': 'Authorization secret was not found.',
'execution.error.secret_invalid': 'Authorization secret has an invalid format.',
'execution.error.input_schema_invalid': 'Input does not satisfy the operation schema.',
'execution.error.input_mapping_invalid': 'Input parameters could not be mapped.',
'execution.error.prepared_request_invalid': 'A valid upstream request could not be prepared.',
'execution.error.execution_overloaded': 'The service is temporarily overloaded.',
'execution.error.safety_store_unavailable': 'A mandatory safety store is unavailable.',
'execution.error.protocol_unsupported': 'The operation protocol is unsupported.',
'execution.error.execution_mode_unsupported': 'The execution mode is unsupported.',
'execution.error.adapter_configuration_invalid': 'The adapter configuration is invalid.',
'execution.error.outbound_target_rejected': 'The target was rejected by the safety policy.',
'execution.error.upstream_auth_error': 'The upstream API rejected authorization.',
'execution.error.upstream_not_found': 'The upstream resource was not found.',
'execution.error.upstream_rate_limited': 'The upstream API rate-limited the request.',
'execution.error.upstream_server_error': 'The upstream API is temporarily unavailable.',
'execution.error.upstream_status_error': 'The upstream API returned an error status.',
'execution.error.upstream_timeout': 'The upstream API timed out.',
'execution.error.upstream_transport_error': 'The upstream API could not be reached.',
'execution.error.upstream_response_too_large': 'The upstream response exceeded its limit.',
'execution.error.output_mapping_invalid': 'The upstream response could not be mapped.',
'execution.error.output_schema_invalid': 'The output does not satisfy the operation schema.',
'execution.error.persistence_unavailable': 'Mandatory result persistence is unavailable.',
'execution.error.runtime_internal': 'Internal execution failure.',
'execution.error.confirmation_required': 'The operation requires confirmation.',
'execution.error.confirmation_invalid': 'The confirmation is invalid or expired.',
'execution.error.idempotency_in_progress': 'The operation is already running for this key.',
'execution.error.idempotency_conflict': 'The idempotency key was used with different input.',
'execution.error.idempotency_outcome_unknown': 'The previous outcome is unknown; automatic retry is unsafe.',
'wizard.test.window_completed': 'Window test completed',
'wizard.test.window_completed_body': 'Collected a bounded result window.',
'wizard.test.window_truncated_note': 'Results were truncated.',
@@ -727,6 +849,7 @@ var TRANSLATIONS = {
'wizard.quality.blocked_title': 'Quality check found blocking issues',
'wizard.quality.blocked_body': 'Fix error-level findings before publishing.',
'wizard.quality.blocking_error': 'Fix blocking quality findings before publishing.',
'wizard.quality.import_findings_hint': 'These findings came from OpenAPI import. Run quality analysis to recalculate them for the current Draft.',
'wizard.quality.severity_error': 'Error',
'wizard.quality.severity_warning': 'Warning',
'wizard.quality.severity_info': 'Info',
@@ -743,6 +866,16 @@ var TRANSLATIONS = {
'wizard.yaml.loaded_body': 'The YAML configuration was loaded.',
'wizard.publish.done': 'Operation published',
'wizard.publish.done_body': 'Version {version} is now published and can be bound into agents.',
'wizard.publish.confirm': 'Publish the saved Draft as a new immutable version?',
'wizard.stale.title': 'Draft changed elsewhere',
'wizard.stale.body': 'Your edits are preserved in this browser. Reload the latest saved Draft before retrying.',
'wizard.test.correlation': 'Request ID: {requestId}\nTrace ID: {traceId}',
'wizard.test.request_id': 'Request ID',
'wizard.test.trace_id': 'Trace ID',
'wizard.test.copy_request_id': 'Copy Request ID',
'wizard.test.copy_trace_id': 'Copy Trace ID',
'wizard.test.id_copied': 'Support ID copied',
'wizard.test.id_copied_body': 'The identifier was copied without request or response payload.',
// Common buttons
'btn.save': 'Save changes',
@@ -785,6 +918,7 @@ var TRANSLATIONS = {
'agents.card.keys': 'keys',
'agents.card.calls_today': 'calls today',
'agents.card.created': 'Created {date}',
'agents.card.revision': 'Draft v{draft} · Published v{published} · Catalog rev {revision}',
'agents.card.copy_endpoint': 'Copy endpoint',
'agents.card.endpoint_help': 'Set this endpoint in the MCP client together with the API key.',
'agents.card.endpoint_help_community': 'Set this endpoint in the MCP client together with the API key.',
@@ -809,6 +943,7 @@ var TRANSLATIONS = {
'agents.drawer.optional': '(optional)',
'agents.drawer.required': 'required',
'agents.drawer.status': 'Status',
'agents.drawer.revision': 'Draft v{draft} · Published v{published} · Catalog rev {revision}',
'agents.drawer.endpoint': 'MCP endpoint',
'agents.drawer.slug_hint': 'Slug is used as part of the endpoint to identify the agent.',
'agents.drawer.placeholder.name': 'Customer Support',
@@ -867,12 +1002,17 @@ var TRANSLATIONS = {
'agents.toast.delete_message': '{name} was deleted.',
'agents.toast.delete_error_title': 'Delete failed',
'agents.toast.delete_error_message': 'Failed to delete agent',
'agents.toast.delete_forbidden_message': 'Published agents cannot be deleted. Archive or unpublish the agent first.',
'agents.toast.lifecycle_title': 'Agent lifecycle updated',
'agents.toast.lifecycle_publish_confirm': 'Publish {name}? The published MCP catalog will change.',
'agents.toast.lifecycle_unpublish_confirm': 'Unpublish {name}? Existing MCP clients will no longer see this agent catalog.',
'agents.toast.lifecycle_archive_confirm': 'Archive {name}? New publications and bindings will be blocked.',
'agents.toast.lifecycle_publish': '{name} was published.',
'agents.toast.lifecycle_unpublish': '{name} was returned to draft.',
'agents.toast.lifecycle_archive': '{name} was archived.',
'agents.toast.lifecycle_error_title': 'Lifecycle update failed',
'agents.toast.lifecycle_error_message': 'Failed to update agent lifecycle',
'agents.toast.stale_message': 'Agent changed in another request. Reload the drawer and retry.',
'agents.toast.endpoint_title': 'MCP endpoint copied',
// Demo content
@@ -889,9 +1029,24 @@ var TRANSLATIONS = {
'login.email_label': 'Email address',
'login.email_placeholder': 'you@acme.com',
'login.password_only': 'Sign in with email and password.',
'login.bootstrap.title': 'Create admin account',
'login.bootstrap.subtitle': 'Complete local first-run bootstrap',
'login.bootstrap.note': 'Enter the one-time token generated by crank-migrate and choose the first admin password.', // community-scope: allow=one-time-token
'login.bootstrap.token_label': 'Bootstrap token',
'login.bootstrap.token_placeholder': 'Paste one-time bootstrap token', // community-scope: allow=one-time-token
'login.bootstrap.submit': 'Create admin',
'login.loading': 'Signing in…',
'login.success': 'Signed in. Redirecting…',
'login.bootstrap.loading': 'Creating admin…',
'login.bootstrap.success': 'Admin account created. Redirecting…',
'login.error.required': 'Please enter your email and password.',
'login.bootstrap.error.required': 'Please enter the bootstrap token and password.',
'login.error.invalid': 'Invalid email or password. Please try again.',
'login.error.throttled': 'Too many attempts. Please wait and try again.',
'login.error.expired_session': 'Your session expired. Please sign in again.',
'login.error.generic': 'Unable to sign in right now. Please try again.',
'login.error.request_id': 'Request ID',
'login.error.trace_id': 'Trace ID',
},
ru: {
@@ -910,6 +1065,43 @@ var TRANSLATIONS = {
'nav.user_fallback': 'Crank',
'nav.workspace_fallback': 'workspace',
'onboarding.title': 'Начало работы',
'onboarding.subtitle': 'Возобновляемый путь к первому реальному вызову MCP-инструмента.',
'onboarding.loading': 'Проверяем подтверждённый прогресс…',
'onboarding.progress': 'Выполнено шагов: {count}/7',
'onboarding.error': 'Прогресс начала работы недоступен.',
'onboarding.retry': 'Повторить',
'onboarding.refresh': 'Обновить прогресс',
'onboarding.dismiss': 'Скрыть',
'onboarding.resume': 'Продолжить начало работы',
'onboarding.collapse': 'Свернуть чек-лист начала работы',
'onboarding.request_id': 'Request ID',
'onboarding.trace_id': 'Trace ID',
'onboarding.error_code': 'Код ошибки',
'onboarding.progress_label': 'Прогресс начала работы',
'onboarding.action_needed': 'Это следующий доступный шаг.',
'onboarding.regressed': 'Связанный объект изменился. Проверьте его и продолжите снова.',
'onboarding.completed': 'Первый публичный вызов MCP-инструмента выполнен.',
'onboarding.open_invocation': 'Открыть историю вызова',
'onboarding.tool': 'Инструмент',
'onboarding.timestamp': 'Время вызова',
'onboarding.deep_link_stale': 'Эта ссылка начала работы относится к изменённой операции или агенту. Сбросьте её и выберите актуальную опубликованную ревизию.',
'onboarding.reselect': 'Сбросить и выбрать заново',
'onboarding.step.operation': 'Создать операцию',
'onboarding.step.test': 'Протестировать операцию',
'onboarding.step.publish_operation': 'Опубликовать операцию',
'onboarding.step.agent': 'Создать и опубликовать агента',
'onboarding.step.key': 'Создать ключ MCP-клиента',
'onboarding.step.mcp_connection': 'Подключить MCP-клиент',
'onboarding.step.first_call': 'Выполнить первый вызов',
'onboarding.action.operation': 'Создать операцию',
'onboarding.action.test': 'Открыть тест',
'onboarding.action.publish_operation': 'Открыть публикацию',
'onboarding.action.agent': 'Создать агента',
'onboarding.action.key': 'Создать ключ',
'onboarding.action.mcp_connection': 'Показать подключение',
'onboarding.action.first_call': 'Открыть логи',
// Operations page
'ops.title': 'Операции',
'ops.subtitle': 'Каталог инструментов. Список созданных MCP инструментов на API эндпоинты.',
@@ -927,6 +1119,12 @@ var TRANSLATIONS = {
'ops.delete.error.message': 'Не удалось удалить операцию',
'ops.action.edit': 'Редактировать операцию',
'ops.action.delete':'Удалить операцию',
'ops.action.archive': 'Архивировать операцию',
'ops.archive.confirm': 'Архивировать операцию? Существующие опубликованные снимки агентов останутся доступны, но новые публикации и привязки будут запрещены.',
'ops.archive.success.title': 'Операция архивирована',
'ops.archive.success.message': 'Операция {name} архивирована.',
'ops.archive.error.title': 'Не удалось архивировать',
'ops.archive.error.message': 'Не удалось архивировать операцию',
'ops.stats.synced': 'Каталог синхронизирован с сервером',
'ops.stats.none': 'Операций пока нет',
'ops.stats.share': '{value}% от общего числа',
@@ -1027,11 +1225,16 @@ var TRANSLATIONS = {
'apikeys.modal.reveal_title': 'Скопируйте ключ сейчас.',
'apikeys.modal.reveal_body': 'Повторно он не будет показан.',
'apikeys.modal.copy': 'Скопировать',
'apikeys.modal.copy_config': 'Скопировать конфигурацию',
'apikeys.modal.connection_title': 'Конфигурация MCP-подключения',
'apikeys.modal.clipboard_warning': 'Системный буфер обмена находится вне контроля Crank. Очистите его после сохранения ключа.',
'apikeys.onboarding.key_lost': 'Ключ нельзя показать повторно. Создайте новый ключ и отзовите старый, если значение не было сохранено.',
'apikeys.modal.cancel': 'Отмена',
'apikeys.modal.create': 'Создать ключ',
'apikeys.modal.done': 'Готово — ключ скопирован',
'apikeys.status.active': 'Активен',
'apikeys.status.revoked': 'Отозван',
'apikeys.status.deleted': 'Удален',
'apikeys.last_used.never': 'Никогда',
'apikeys.empty.none': 'API-ключей пока нет',
'apikeys.empty.approval': 'Ключей подтверждения пока нет. Они нужны только агентам с инструментами, требующими подтверждения человеком.',
@@ -1057,12 +1260,18 @@ var TRANSLATIONS = {
'apikeys.toast.create_error_message': 'Не удалось создать ключ',
'apikeys.toast.copy_title': 'Ключ агента скопирован',
'apikeys.toast.copy_message': 'Сохраните исходный ключ в надежном месте. Повторно показать его нельзя.',
'apikeys.toast.copy_error_title': 'Не удалось записать в буфер обмена',
'apikeys.toast.copy_error_message': 'Одноразовое значение всё ещё показано. Скопируйте его вручную до закрытия страницы.',
'apikeys.toast.prefix_title': 'Префикс ключа скопирован',
'apikeys.action.copy_prefix': 'Скопировать префикс ключа',
'apikeys.action.revoke': 'Отозвать ключ',
'apikeys.action.delete': 'Удалить',
'apikeys.creating': 'Создание…',
'apikeys.approval.warning': 'Не передавайте этот ключ LLM или MCP-клиенту. Он нужен только внешнему интерфейсу, где человек подтверждает действие.',
'apikeys.ambiguous.title': 'Результат создания неизвестен.',
'apikeys.ambiguous.body': 'Метаданные ключей обновлены. Проверьте список перед осознанным созданием ещё одного ключа.',
'apikeys.ambiguous.retry': 'Обновить метаданные и разрешить новую попытку',
'apikeys.ambiguous.blocked': 'Проверьте метаданные перед повтором',
// Secrets page
'secrets.title': 'Секреты',
@@ -1148,6 +1357,16 @@ var TRANSLATIONS = {
'logs.live': 'Live',
'logs.paused': 'Пауза',
'logs.refresh': 'Обновить',
'logs.export': 'Экспорт CSV',
'logs.load_more': 'Загрузить ещё',
'logs.status_filter': 'Статус',
'logs.status.all': 'Все статусы',
'logs.status.ok': 'Успех',
'logs.status.error': 'Ошибка',
'logs.outcome_filter': 'Результат',
'logs.outcome.all': 'Все результаты',
'logs.filter.operation': 'ID операции',
'logs.filter.agent': 'ID агента',
'logs.range.30m': 'Последние 30 мин',
'logs.range.1h': 'Последний час',
'logs.range.6h': 'Последние 6 часов',
@@ -1173,6 +1392,10 @@ var TRANSLATIONS = {
'logs.live.off.body': 'Автоматический опрос остановлен.',
'logs.refresh.title': 'Логи обновлены',
'logs.refresh.body': 'Получены последние записи вызовов для текущего воркспейса.',
'logs.export.done_title': 'Логи экспортированы',
'logs.export.done_body': 'Отфильтрованная история вызовов экспортирована в CSV.',
'logs.export.error_title': 'Не удалось экспортировать',
'logs.export.error_body': 'Не удалось экспортировать историю вызовов.',
'approvals.title': 'Подтверждения человеком',
'approvals.subtitle': 'Заявки, которые ожидают решения пользователя, и последние результаты.',
'approvals.refresh': 'Обновить',
@@ -1181,6 +1404,7 @@ var TRANSLATIONS = {
'approvals.loading': 'Загрузка заявок на подтверждение…',
'approvals.empty': 'Заявок на подтверждение пока нет.',
'approvals.error.load': 'Не удалось загрузить заявки на подтверждение',
'approvals.error.decision': 'Не удалось отправить решение по заявке',
'approvals.untitled': 'Заявка на подтверждение',
'approvals.operation': 'Операция',
'approvals.agent': 'Агент',
@@ -1188,6 +1412,13 @@ var TRANSLATIONS = {
'approvals.updated_at': 'Обновлено',
'approvals.request': 'Запрос',
'approvals.response': 'Результат',
'approvals.request_id': 'Request ID',
'approvals.trace_id': 'Trace ID',
'approvals.action.approve': 'Подтвердить',
'approvals.action.deny': 'Отклонить',
'approvals.action.busy': 'Отправка…',
'approvals.confirm.approve': 'Подтвердить эту заявку?',
'approvals.confirm.deny': 'Отклонить эту заявку?',
'approvals.status.pending': 'Ожидает',
'approvals.status.approved': 'Подтверждено',
'approvals.status.denied': 'Отклонено',
@@ -1220,6 +1451,19 @@ var TRANSLATIONS = {
'usage.chart.error': 'Ошибка',
'usage.chart.empty.title': 'Данных по использованию пока нет',
'usage.chart.empty.sub': 'Метрики появятся здесь после тестов или вызовов опубликованных инструментов за выбранный период.',
'usage.outcomes.title': 'Исходы',
'usage.outcomes.subtitle': 'Группировка по безопасному исходу выполнения.',
'usage.outcomes.empty.title': 'Разбивки по исходам пока нет',
'usage.outcomes.empty.sub': 'Группы исходов появятся после записи вызовов.',
'usage.outcomes.p50': 'p50',
'usage.outcomes.p95': 'p95',
'usage.outcomes.p99': 'p99',
'usage.outcome.success': 'Успех',
'usage.outcome.upstream': 'Upstream',
'usage.outcome.client': 'Клиент',
'usage.outcome.schema': 'Схема',
'usage.outcome.crank': 'Crank',
'usage.outcome.no_error_code': 'Без error code',
'usage.table.title': 'По операциям',
'usage.table.subtitle': 'Разбивка за период: {period}',
'usage.table.th.operation': 'Операция',
@@ -1241,7 +1485,9 @@ var TRANSLATIONS = {
'usage.export.empty.title': 'Данные по использованию не загружены',
'usage.export.empty.body': 'Сначала загрузите данные использования, а потом экспортируйте снимок CSV.',
'usage.export.done.title': 'Использование экспортировано',
'usage.export.done.body': 'Текущий снимок использования экспортирован в CSV.',
'usage.export.done.body': 'Отфильтрованный набор использования экспортирован в CSV.',
'usage.export.error.title': 'Не удалось экспортировать',
'usage.export.error.body': 'Не удалось экспортировать данные использования.',
'usage.chart.ok': 'Успешных: {count}',
'usage.chart.errors': 'Ошибок: {count}',
'usage.chart.week': 'Нед. {index}',
@@ -1295,6 +1541,7 @@ var TRANSLATIONS = {
'settings.security.change_password': 'Сменить пароль',
'settings.security.mismatch': 'Новый пароль и подтверждение не совпадают.',
'settings.security.saved': 'Пароль обновлен.',
'settings.security.saved_relogin': 'Пароль обновлен. Войдите снова.',
'settings.security.save_error': 'Не удалось изменить пароль',
'settings.capability.rest': 'REST / HTTP',
'settings.capability.standard': 'standard',
@@ -1619,6 +1866,7 @@ var TRANSLATIONS = {
'wizard.quality.blocked_title': 'Проверка нашла блокирующие ошибки',
'wizard.quality.blocked_body': 'Исправьте замечания уровня «Ошибка» перед публикацией.',
'wizard.quality.blocking_error': 'Исправьте блокирующие замечания качества перед публикацией.',
'wizard.quality.import_findings_hint': 'Эти замечания получены при импорте OpenAPI. Запустите проверку качества, чтобы пересчитать их для текущего черновика.',
'wizard.quality.severity_error': 'Ошибка',
'wizard.quality.severity_warning': 'Предупреждение',
'wizard.quality.severity_info': 'Информация',
@@ -1635,6 +1883,16 @@ var TRANSLATIONS = {
'wizard.yaml.loaded_body': 'YAML-конфигурация загружена.',
'wizard.publish.done': 'Операция опубликована',
'wizard.publish.done_body': 'Версия {version} опубликована и теперь может быть привязана к агентам.',
'wizard.publish.confirm': 'Опубликовать сохранённый черновик как новую неизменяемую версию?',
'wizard.stale.title': 'Черновик изменён в другом окне',
'wizard.stale.body': 'Ваши правки сохранены в этом браузере. Перезагрузите последнюю сохранённую версию перед повторной попыткой.',
'wizard.test.correlation': 'Request ID: {requestId}\nTrace ID: {traceId}',
'wizard.test.request_id': 'Request ID',
'wizard.test.trace_id': 'Trace ID',
'wizard.test.copy_request_id': 'Копировать Request ID',
'wizard.test.copy_trace_id': 'Копировать Trace ID',
'wizard.test.id_copied': 'Идентификатор поддержки скопирован',
'wizard.test.id_copied_body': 'Идентификатор скопирован без содержимого запроса или ответа.',
// Common buttons
'btn.save': 'Сохранить',
@@ -1677,6 +1935,7 @@ var TRANSLATIONS = {
'agents.card.keys': 'ключей',
'agents.card.calls_today': 'вызовов сегодня',
'agents.card.created': 'Создан {date}',
'agents.card.revision': 'Черновик v{draft} · Опубликована v{published} · Ревизия каталога {revision}',
'agents.card.copy_endpoint': 'Скопировать endpoint',
'agents.card.endpoint_help': 'Данный эндпоинт требуется указать на стороне MCP клиента вместе с API ключом.',
'agents.card.endpoint_help_community': 'Данный эндпоинт требуется указать на стороне MCP клиента вместе с API ключом.',
@@ -1701,6 +1960,7 @@ var TRANSLATIONS = {
'agents.drawer.optional': '(необязательно)',
'agents.drawer.required': 'обязательно',
'agents.drawer.status': 'Статус',
'agents.drawer.revision': 'Черновик v{draft} · Опубликована v{published} · Ревизия каталога {revision}',
'agents.drawer.endpoint': 'MCP endpoint',
'agents.drawer.slug_hint': 'Slug используется как часть endpoint-а для идентификации агента.',
'agents.drawer.placeholder.name': 'Customer Support',
@@ -1759,12 +2019,17 @@ var TRANSLATIONS = {
'agents.toast.delete_message': 'Агент {name} удален.',
'agents.toast.delete_error_title': 'Не удалось удалить',
'agents.toast.delete_error_message': 'Не удалось удалить агента',
'agents.toast.delete_forbidden_message': 'Опубликованных агентов нельзя удалить. Сначала архивируйте или снимите агента с публикации.',
'agents.toast.lifecycle_title': 'Жизненный цикл агента обновлен',
'agents.toast.lifecycle_publish_confirm': 'Опубликовать {name}? Опубликованный MCP-каталог изменится.',
'agents.toast.lifecycle_unpublish_confirm': 'Снять {name} с публикации? Текущие MCP-клиенты больше не увидят этот каталог агента.',
'agents.toast.lifecycle_archive_confirm': 'Архивировать {name}? Новые публикации и привязки будут заблокированы.',
'agents.toast.lifecycle_publish': '{name} опубликован.',
'agents.toast.lifecycle_unpublish': '{name} возвращен в черновик.',
'agents.toast.lifecycle_archive': '{name} архивирован.',
'agents.toast.lifecycle_error_title': 'Не удалось обновить жизненный цикл',
'agents.toast.lifecycle_error_message': 'Не удалось обновить состояние агента',
'agents.toast.stale_message': 'Агент изменился в другом запросе. Перезагрузите форму и повторите действие.',
'agents.toast.endpoint_title': 'MCP endpoint скопирован',
// Demo content
@@ -1781,9 +2046,24 @@ var TRANSLATIONS = {
'login.email_label': 'Email адрес',
'login.email_placeholder': 'you@acme.com',
'login.password_only': 'Войдите с помощью email и пароля.',
'login.bootstrap.title': 'Создать администратора',
'login.bootstrap.subtitle': 'Завершите локальную первичную настройку',
'login.bootstrap.note': 'Введите одноразовый токен из crank-migrate и задайте первый пароль администратора.',
'login.bootstrap.token_label': 'Bootstrap token',
'login.bootstrap.token_placeholder': 'Вставьте одноразовый bootstrap token',
'login.bootstrap.submit': 'Создать администратора',
'login.loading': 'Входим…',
'login.success': 'Вход выполнен. Перенаправляем…',
'login.bootstrap.loading': 'Создаем администратора…',
'login.bootstrap.success': 'Администратор создан. Перенаправляем…',
'login.error.required': 'Введите email и пароль.',
'login.bootstrap.error.required': 'Введите bootstrap token и пароль.',
'login.error.invalid': 'Неверный email или пароль. Попробуйте еще раз.',
'login.error.throttled': 'Слишком много попыток. Подождите и попробуйте снова.',
'login.error.expired_session': 'Сессия истекла. Войдите снова.',
'login.error.generic': 'Сейчас не удается войти. Попробуйте еще раз.',
'login.error.request_id': 'Request ID',
'login.error.trace_id': 'Trace ID',
}
};
@@ -1793,6 +2073,36 @@ Object.assign(TRANSLATIONS.en, {
});
Object.assign(TRANSLATIONS.ru, {
'execution.error.authorization_denied': 'Выполнение операции запрещено.',
'execution.error.auth_profile_not_found': 'Профиль авторизации не найден.',
'execution.error.secret_not_found': 'Секрет авторизации не найден.',
'execution.error.secret_invalid': 'Секрет авторизации имеет неподходящий формат.',
'execution.error.input_schema_invalid': 'Входные параметры не прошли проверку схемы.',
'execution.error.input_mapping_invalid': 'Не удалось сопоставить входные параметры.',
'execution.error.prepared_request_invalid': 'Не удалось подготовить корректный API-запрос.',
'execution.error.execution_overloaded': 'Сервис временно перегружен.',
'execution.error.safety_store_unavailable': 'Обязательное хранилище безопасности недоступно.',
'execution.error.protocol_unsupported': 'Протокол операции не поддерживается.',
'execution.error.execution_mode_unsupported': 'Режим выполнения не поддерживается.',
'execution.error.adapter_configuration_invalid': 'Конфигурация адаптера некорректна.',
'execution.error.outbound_target_rejected': 'Целевой адрес отклонён политикой безопасности.',
'execution.error.upstream_auth_error': 'Внешний API отклонил авторизацию.',
'execution.error.upstream_not_found': 'Ресурс внешнего API не найден.',
'execution.error.upstream_rate_limited': 'Внешний API ограничил частоту запросов.',
'execution.error.upstream_server_error': 'Внешний API временно недоступен.',
'execution.error.upstream_status_error': 'Внешний API вернул ошибочный статус.',
'execution.error.upstream_timeout': 'Истекло время ожидания внешнего API.',
'execution.error.upstream_transport_error': 'Не удалось подключиться к внешнему API.',
'execution.error.upstream_response_too_large': 'Ответ внешнего API превышает лимит.',
'execution.error.output_mapping_invalid': 'Не удалось сопоставить ответ внешнего API.',
'execution.error.output_schema_invalid': 'Ответ не прошёл проверку схемы.',
'execution.error.persistence_unavailable': 'Обязательное сохранение результата недоступно.',
'execution.error.runtime_internal': 'Внутренняя ошибка выполнения.',
'execution.error.confirmation_required': 'Операция требует подтверждения.',
'execution.error.confirmation_invalid': 'Подтверждение недействительно или истекло.',
'execution.error.idempotency_in_progress': 'Операция с этим ключом уже выполняется.',
'execution.error.idempotency_conflict': 'Ключ идемпотентности использован с другими параметрами.',
'execution.error.idempotency_outcome_unknown': 'Результат предыдущего выполнения неизвестен; автоматический повтор запрещён.',
});
+73 -5
View File
@@ -3,6 +3,17 @@
return window.t ? t(key) : key;
}
function diagnosticSuffix(error) {
var parts = [];
if (error && error.requestId) {
parts.push(tKey('login.error.request_id') + ': ' + error.requestId);
}
if (error && error.traceId) {
parts.push(tKey('login.error.trace_id') + ': ' + error.traceId);
}
return parts.length ? ('\n' + parts.join('\n')) : '';
}
function showError(message) {
var errorElement = document.getElementById('login-error');
errorElement.classList.add('is-visible');
@@ -14,34 +25,91 @@
errorElement.classList.remove('is-visible');
}
function setBootstrapMode(enabled) {
var emailField = document.getElementById('login-email-field');
var tokenField = document.getElementById('login-bootstrap-token-field');
var heading = document.querySelector('.login-heading');
var subtitle = document.querySelector('.login-sub');
var note = document.querySelector('.login-note');
var submit = document.getElementById('login-submit');
var password = document.getElementById('password');
document.getElementById('login-form').dataset.bootstrap = enabled ? 'true' : 'false';
if (emailField) emailField.hidden = enabled;
if (tokenField) tokenField.hidden = !enabled;
if (heading) heading.textContent = tKey(enabled ? 'login.bootstrap.title' : 'login.title');
if (subtitle) subtitle.textContent = tKey(enabled ? 'login.bootstrap.subtitle' : 'login.subtitle');
if (note) note.textContent = tKey(enabled ? 'login.bootstrap.note' : 'login.password_only');
if (submit) submit.textContent = tKey(enabled ? 'login.bootstrap.submit' : 'login.submit');
if (password) password.setAttribute('autocomplete', enabled ? 'new-password' : 'current-password');
}
function initLoginPage() {
var inFlight = false;
window.CrankAuth.guardLoginPage().catch(function(error) {
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.report === 'function') {
window.CrankDiagnostics.report('guard-login-page', error, 'login');
}
});
setBootstrapMode(false);
window.CrankApi.getBootstrapStatus()
.then(function(status) {
setBootstrapMode(Boolean(status && status.bootstrap_required));
})
.catch(function(error) {
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.report === 'function') {
window.CrankDiagnostics.report('bootstrap-status', error, 'login');
}
});
document.getElementById('login-form').addEventListener('submit', async function(event) {
event.preventDefault();
if (inFlight) {
return;
}
var form = event.currentTarget;
var isBootstrap = event.currentTarget.dataset.bootstrap === 'true';
var email = document.getElementById('email').value.trim();
var token = document.getElementById('bootstrap-token').value.trim();
var password = document.getElementById('password').value;
var submit = document.getElementById('login-submit');
if (!email || !password) {
showError(tKey('login.error.required'));
if ((!isBootstrap && !email) || (isBootstrap && !token) || !password) {
showError(tKey(isBootstrap ? 'login.bootstrap.error.required' : 'login.error.required'));
return;
}
hideError();
inFlight = true;
if (submit) {
submit.disabled = true;
submit.textContent = tKey(isBootstrap ? 'login.bootstrap.loading' : 'login.loading');
}
try {
await window.CrankAuth.login(email, password);
if (isBootstrap) {
await window.CrankAuth.completeBootstrap(token, password);
} else {
await window.CrankAuth.login(email, password);
}
showError(tKey(isBootstrap ? 'login.bootstrap.success' : 'login.success'));
} catch (error) {
if (error && error.status === 401) {
showError(tKey('login.error.invalid'));
showError(tKey('login.error.invalid') + diagnosticSuffix(error));
return;
}
showError(tKey('login.error.generic'));
if (error && error.status === 429) {
showError(tKey('login.error.throttled') + diagnosticSuffix(error));
return;
}
showError(tKey('login.error.generic') + diagnosticSuffix(error));
} finally {
inFlight = false;
if (submit && form.isConnected) {
submit.disabled = false;
submit.textContent = tKey(isBootstrap ? 'login.bootstrap.submit' : 'login.submit');
}
}
});
}
+297 -14
View File
@@ -3,9 +3,14 @@ document.addEventListener('DOMContentLoaded', function () {
logs: [],
details: {},
level: 'all',
status: 'all',
outcomeGroup: 'all',
operationId: '',
agentId: '',
search: '',
period: '7d',
openId: null,
nextCursor: null,
liveMode: true,
timer: null,
searchTimer: null,
@@ -16,6 +21,10 @@ document.addEventListener('DOMContentLoaded', function () {
approvals: [],
approvalsLoading: false,
approvalsError: '',
approvalActionBusy: {},
approvalsEpoch: 0,
logsEpoch: 0,
exporting: false,
};
var logList = document.getElementById('log-list');
@@ -23,9 +32,21 @@ document.addEventListener('DOMContentLoaded', function () {
var approvalRefreshBtn = document.getElementById('approval-refresh-btn');
var logSearch = document.getElementById('log-search');
var refreshBtn = document.getElementById('refresh-btn');
var exportLogsBtn = document.getElementById('export-logs-btn');
var loadMoreBtn = document.getElementById('load-more-logs-btn');
var timeRangeSel = document.getElementById('time-range');
var statusFilter = document.getElementById('status-filter');
var outcomeFilter = document.getElementById('outcome-filter');
var operationFilter = document.getElementById('operation-filter');
var agentFilter = document.getElementById('agent-filter');
var liveDot = document.querySelector('.live-dot');
var liveLabel = document.querySelector('.live-label');
var initialParams = new URLSearchParams(window.location.search);
var deepLinkLogId = /^[A-Za-z0-9_-]{1,128}$/.test(initialParams.get('log_id') || '') ? initialParams.get('log_id') : '';
var deepLinkAgentId = /^[A-Za-z0-9_-]{1,128}$/.test(initialParams.get('agent_id') || '') ? initialParams.get('agent_id') : '';
var deepLinkOperationId = /^[A-Za-z0-9_-]{1,128}$/.test(initialParams.get('operation_id') || '') ? initialParams.get('operation_id') : '';
state.agentId = deepLinkAgentId;
state.operationId = deepLinkOperationId;
function tKey(key) {
return window.t ? t(key) : key;
@@ -118,6 +139,7 @@ document.addEventListener('DOMContentLoaded', function () {
status: log.status,
statusCode: log.status_code,
durationMs: log.duration_ms,
operationVersion: log.operation_version,
toolName: log.tool_name,
message: log.message,
operationName: record.operation_name,
@@ -127,7 +149,10 @@ document.addEventListener('DOMContentLoaded', function () {
requestPreview: log.request_preview,
responsePreview: log.response_preview,
errorKind: log.error_kind,
executionStage: log.execution_stage,
executionErrorCode: log.execution_error_code,
requestId: log.request_id,
traceId: log.trace_id,
};
}
@@ -142,6 +167,8 @@ document.addEventListener('DOMContentLoaded', function () {
riskLevel: approval.risk_level,
requestPayload: approval.request_payload,
responsePayload: approval.response_payload,
requestId: approval.request_id,
traceId: approval.trace_id,
createdAt: approval.created_at,
expiresAt: approval.expires_at,
decidedAt: approval.decided_at,
@@ -206,6 +233,17 @@ document.addEventListener('DOMContentLoaded', function () {
: tKey('approvals.updated_at') + ': ' + formatDateTime(item.decidedAt || item.createdAt);
card.appendChild(timing);
if (item.requestId || item.traceId) {
var correlation = element('div', 'approval-correlation');
if (item.requestId) {
correlation.appendChild(element('span', 'approval-correlation-id', tKey('approvals.request_id') + ': ' + item.requestId));
}
if (item.traceId) {
correlation.appendChild(element('span', 'approval-correlation-id', tKey('approvals.trace_id') + ': ' + item.traceId));
}
card.appendChild(correlation);
}
var payloadGrid = element('div', 'approval-payload-grid');
var requestBlock = element('div', 'approval-payload');
requestBlock.appendChild(element('div', 'approval-payload-label', tKey('approvals.request')));
@@ -228,6 +266,24 @@ document.addEventListener('DOMContentLoaded', function () {
card.appendChild(element('div', 'approval-note', item.note));
}
if (item.status === 'pending') {
var actions = element('div', 'approval-actions');
var busy = Boolean(state.approvalActionBusy[item.id]);
var approve = element('button', 'btn btn-primary btn-sm approval-approve', busy ? tKey('approvals.action.busy') : tKey('approvals.action.approve'));
approve.type = 'button';
approve.disabled = busy;
approve.dataset.approvalId = item.id;
approve.dataset.action = 'approve';
var deny = element('button', 'btn btn-secondary btn-sm approval-deny', busy ? tKey('approvals.action.busy') : tKey('approvals.action.deny'));
deny.type = 'button';
deny.disabled = busy;
deny.dataset.approvalId = item.id;
deny.dataset.action = 'deny';
actions.appendChild(approve);
actions.appendChild(deny);
card.appendChild(actions);
}
fragment.appendChild(card);
});
@@ -256,10 +312,13 @@ document.addEventListener('DOMContentLoaded', function () {
if (!state.logs.length) {
renderEmpty(
tKey('logs.empty.title'),
state.search || state.level !== 'all'
state.search || state.level !== 'all' || state.status !== 'all' || state.outcomeGroup !== 'all' || state.operationId || state.agentId
? tKey('logs.empty.filtered')
: tKey('logs.empty.initial')
);
if (loadMoreBtn) {
loadMoreBtn.hidden = true;
}
return;
}
@@ -347,7 +406,7 @@ document.addEventListener('DOMContentLoaded', function () {
responsePre.textContent = formatJson(detail.responsePreview);
expanded.appendChild(responsePre);
if (detail.errorKind || detail.requestId) {
if (detail.errorKind || detail.requestId || detail.traceId || detail.executionStage || detail.executionErrorCode || detail.operationVersion) {
var metaLabel = document.createElement('div');
metaLabel.className = 'log-detail-label';
metaLabel.textContent = tKey('logs.detail.meta');
@@ -357,7 +416,11 @@ document.addEventListener('DOMContentLoaded', function () {
metaPre.className = 'log-detail-block';
metaPre.textContent = formatJson({
request_id: detail.requestId || null,
trace_id: detail.traceId || null,
operation_version: detail.operationVersion || null,
error_kind: detail.errorKind || null,
execution_stage: detail.executionStage || null,
execution_error_code: detail.executionErrorCode || null,
source: detail.source,
status: detail.status,
});
@@ -370,6 +433,10 @@ document.addEventListener('DOMContentLoaded', function () {
logList.innerHTML = '';
logList.appendChild(fragment);
if (loadMoreBtn) {
loadMoreBtn.hidden = !state.nextCursor;
loadMoreBtn.disabled = state.loading;
}
logList.querySelectorAll('.log-entry').forEach(function (row) {
row.addEventListener('click', async function () {
@@ -383,21 +450,60 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
function queryParams() {
function invalidateLogsRequest() {
state.logsEpoch += 1;
state.nextCursor = null;
}
function queryParams(options) {
options = options || {};
var params = {
period: state.period,
limit: 100,
};
if (options.includeLimit !== false) {
params.limit = 100;
}
if (state.level !== 'all') {
params.level = state.level;
}
if (state.search) {
params.search = state.search;
}
if (state.status !== 'all') {
params.status = state.status;
}
if (state.outcomeGroup !== 'all') {
params.outcome_group = state.outcomeGroup;
}
if (state.operationId) {
params.operation_id = state.operationId;
}
if (state.agentId) {
params.agent_id = state.agentId;
}
return params;
}
async function loadLogs() {
function bindTextFilter(input, key) {
if (!input) {
return;
}
input.value = state[key] || '';
input.addEventListener('input', function () {
state[key] = this.value.trim();
invalidateLogsRequest();
if (state.searchTimer) {
clearTimeout(state.searchTimer);
}
state.searchTimer = setTimeout(function () {
state.searchTimer = null;
loadLogs();
}, 250);
});
}
async function loadLogs(options) {
options = options || {};
if (!window.CrankApi) {
state.loadError = tKey('logs.error.api');
renderLogs();
@@ -411,21 +517,39 @@ document.addEventListener('DOMContentLoaded', function () {
return;
}
var workspaceId = state.workspaceId;
var epoch = ++state.logsEpoch;
var append = Boolean(options.append && state.nextCursor);
var params = queryParams();
if (append) {
params.cursor = state.nextCursor;
}
state.loading = true;
state.loadError = '';
renderLogs();
try {
var response = await window.CrankApi.listLogs(state.workspaceId, queryParams());
state.logs = (response && response.items ? response.items : []).map(normalizeLog);
var response = await window.CrankApi.listLogs(workspaceId, params);
if (epoch !== state.logsEpoch || workspaceId !== currentWorkspaceId()) {
return;
}
var loaded = (response && response.items ? response.items : []).map(normalizeLog);
state.logs = append ? state.logs.concat(loaded) : loaded;
state.nextCursor = response && response.next_cursor ? response.next_cursor : null;
if (state.openId && !state.logs.some(function (item) { return item.id === state.openId; })) {
state.openId = null;
}
} catch (error) {
if (epoch !== state.logsEpoch) {
return;
}
state.loadError = error.message || tKey('logs.error.load');
} finally {
state.loading = false;
renderLogs();
if (epoch === state.logsEpoch) {
state.loading = false;
renderLogs();
}
}
}
@@ -443,17 +567,54 @@ document.addEventListener('DOMContentLoaded', function () {
return;
}
var epoch = ++state.approvalsEpoch;
var workspaceId = state.workspaceId;
state.approvalsLoading = true;
state.approvalsError = '';
renderApprovals();
try {
var response = await window.CrankApi.listApprovals(state.workspaceId, { limit: 20 });
var response = await window.CrankApi.listApprovals(workspaceId, { limit: 20 });
if (epoch !== state.approvalsEpoch || workspaceId !== currentWorkspaceId()) {
return;
}
state.approvals = (response && response.items ? response.items : []).map(normalizeApproval);
} catch (error) {
if (epoch !== state.approvalsEpoch) {
return;
}
state.approvalsError = error.message || tKey('approvals.error.load');
} finally {
state.approvalsLoading = false;
if (epoch === state.approvalsEpoch) {
state.approvalsLoading = false;
renderApprovals();
}
}
}
async function decideApproval(approvalId, action) {
if (!window.CrankApi || !state.workspaceId || !approvalId) return;
if (state.approvalActionBusy[approvalId]) return;
var confirmKey = action === 'approve' ? 'approvals.confirm.approve' : 'approvals.confirm.deny';
if (!window.confirm(tKey(confirmKey))) return;
var workspaceId = state.workspaceId;
var epoch = ++state.approvalsEpoch;
state.approvalActionBusy[approvalId] = true;
state.approvalsError = '';
renderApprovals();
try {
if (action === 'approve') {
await window.CrankApi.approveApproval(workspaceId, approvalId, { approve: 'yes' });
} else {
await window.CrankApi.denyApproval(workspaceId, approvalId, { approve: 'no' });
}
if (workspaceId !== currentWorkspaceId() || epoch !== state.approvalsEpoch) return;
state.approvalActionBusy[approvalId] = false;
await loadApprovals();
} catch (error) {
if (workspaceId !== currentWorkspaceId() || epoch !== state.approvalsEpoch) return;
state.approvalsError = error.message || tKey('approvals.error.decision');
state.approvalActionBusy[approvalId] = false;
renderApprovals();
}
}
@@ -473,8 +634,13 @@ document.addEventListener('DOMContentLoaded', function () {
return;
}
var workspaceId = state.workspaceId;
var epoch = state.logsEpoch;
try {
var record = await window.CrankApi.getLog(state.workspaceId, logId);
var record = await window.CrankApi.getLog(workspaceId, logId);
if (epoch !== state.logsEpoch || workspaceId !== currentWorkspaceId()) {
return;
}
state.details[logId] = normalizeLog(record);
if (state.openId === logId) {
renderLogs();
@@ -483,6 +649,32 @@ document.addEventListener('DOMContentLoaded', function () {
}
}
async function loadExactLog(logId) {
var workspaceId = currentWorkspaceId();
var epoch = ++state.logsEpoch;
state.workspaceId = workspaceId;
state.loading = true;
state.loadError = '';
renderLogs();
try {
var record = await window.CrankApi.getLog(workspaceId, logId);
if (epoch !== state.logsEpoch || workspaceId !== currentWorkspaceId()) return;
var detail = normalizeLog(record);
state.logs = [detail];
state.details[logId] = detail;
state.openId = logId;
state.nextCursor = null;
} catch (error) {
if (epoch !== state.logsEpoch) return;
state.loadError = error.message || tKey('logs.error.load');
} finally {
if (epoch === state.logsEpoch) {
state.loading = false;
renderLogs();
}
}
}
function setLiveState() {
if (liveDot) {
liveDot.classList.toggle('is-paused', !state.liveMode);
@@ -524,9 +716,52 @@ document.addEventListener('DOMContentLoaded', function () {
}
}
async function exportLogsCsv() {
if (!window.CrankApi || state.exporting) {
return;
}
var workspaceId = currentWorkspaceId();
if (!workspaceId) {
if (window.CrankUi) {
window.CrankUi.error(tKey('logs.error.workspace'), tKey('logs.export.error_title'));
}
return;
}
state.exporting = true;
if (exportLogsBtn) {
exportLogsBtn.disabled = true;
}
try {
var csv = await window.CrankApi.exportLogsCsv(workspaceId, queryParams({ includeLimit: false }));
if (workspaceId !== currentWorkspaceId()) {
return;
}
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = 'crank-invocation-history.csv';
link.click();
URL.revokeObjectURL(url);
if (window.CrankUi) {
window.CrankUi.success(tKey('logs.export.done_body'), tKey('logs.export.done_title'));
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('logs.export.error_body'), tKey('logs.export.error_title'));
}
} finally {
state.exporting = false;
if (exportLogsBtn) {
exportLogsBtn.disabled = false;
}
}
}
document.querySelectorAll('.filter-chip[data-level]').forEach(function (button) {
button.addEventListener('click', function () {
state.level = this.getAttribute('data-level');
invalidateLogsRequest();
document.querySelectorAll('.filter-chip[data-level]').forEach(function (item) {
item.classList.remove('active');
});
@@ -538,6 +773,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (logSearch) {
logSearch.addEventListener('input', function () {
state.search = this.value.trim();
invalidateLogsRequest();
if (state.searchTimer) {
clearTimeout(state.searchTimer);
}
@@ -548,8 +784,30 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
if (statusFilter) {
statusFilter.value = state.status;
statusFilter.addEventListener('change', function () {
state.status = this.value || 'all';
invalidateLogsRequest();
loadLogs();
});
}
if (outcomeFilter) {
outcomeFilter.value = state.outcomeGroup;
outcomeFilter.addEventListener('change', function () {
state.outcomeGroup = this.value || 'all';
invalidateLogsRequest();
loadLogs();
});
}
bindTextFilter(operationFilter, 'operationId');
bindTextFilter(agentFilter, 'agentId');
if (refreshBtn) {
refreshBtn.addEventListener('click', function () {
invalidateLogsRequest();
loadLogs().then(function () {
if (!state.loadError && window.CrankUi) {
window.CrankUi.info(tKey('logs.refresh.body'), tKey('logs.refresh.title'));
@@ -558,6 +816,16 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
if (exportLogsBtn) {
exportLogsBtn.addEventListener('click', exportLogsCsv);
}
if (loadMoreBtn) {
loadMoreBtn.addEventListener('click', function () {
loadLogs({ append: true });
});
}
if (approvalRefreshBtn) {
approvalRefreshBtn.addEventListener('click', function () {
loadApprovals().then(function () {
@@ -568,10 +836,19 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
if (approvalList) {
approvalList.addEventListener('click', function (event) {
var button = event.target.closest('button[data-approval-id][data-action]');
if (!button) return;
decideApproval(button.dataset.approvalId, button.dataset.action);
});
}
if (timeRangeSel) {
timeRangeSel.value = state.period;
timeRangeSel.addEventListener('change', function () {
state.period = this.value;
invalidateLogsRequest();
loadLogs();
});
}
@@ -585,8 +862,10 @@ document.addEventListener('DOMContentLoaded', function () {
}
window.addEventListener('crank:workspacechange', function () {
state.logsEpoch += 1;
state.details = {};
state.openId = null;
state.nextCursor = null;
refreshOperationalData();
});
@@ -612,8 +891,12 @@ document.addEventListener('DOMContentLoaded', function () {
startPolling();
if (window.whenWorkspacesReady) {
window.whenWorkspacesReady().finally(refreshOperationalData);
window.whenWorkspacesReady().finally(function() {
if (deepLinkLogId) return Promise.all([loadExactLog(deepLinkLogId), loadApprovals()]);
return refreshOperationalData();
});
} else {
refreshOperationalData();
if (deepLinkLogId) Promise.all([loadExactLog(deepLinkLogId), loadApprovals()]);
else refreshOperationalData();
}
});
+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); });
}());
+89 -15
View File
@@ -13,6 +13,10 @@ function initSecretsPage() {
error: '',
modalMode: 'create',
modalSecretId: null,
modalSubmitting: false,
deleting: {},
generation: 0,
modalGeneration: 0,
};
var modal = document.getElementById('secret-modal');
@@ -107,6 +111,10 @@ function initSecretsPage() {
}
function openModal(mode, secret) {
if (state.modalSubmitting) {
return;
}
state.modalGeneration += 1;
state.modalMode = mode;
state.modalSecretId = secret ? secret.id : null;
resetModalFields();
@@ -143,8 +151,16 @@ function initSecretsPage() {
function closeModal() {
modal.classList.remove('open');
state.modalGeneration += 1;
state.modalSubmitting = false;
state.modalMode = 'create';
state.modalSecretId = null;
resetModalFields();
modalName.disabled = false;
modalKind.disabled = false;
modalSubmit.disabled = state.modalSubmitting;
modalSubmit.textContent = tKey('secrets.modal.create_action');
renderSecrets();
}
function updateKindFields() {
@@ -290,6 +306,7 @@ function initSecretsPage() {
rotateButton.textContent = tKey('secrets.action.rotate');
rotateButton.setAttribute('data-testid', 'secret-rotate-action');
rotateButton.setAttribute('data-secret-id', secret.id);
rotateButton.disabled = Boolean(state.deleting[secret.id]) || state.modalSubmitting;
rotateButton.addEventListener('click', function () {
openModal('rotate', secret);
});
@@ -301,6 +318,7 @@ function initSecretsPage() {
deleteButton.textContent = tKey('secrets.action.delete');
deleteButton.setAttribute('data-testid', 'secret-delete-action');
deleteButton.setAttribute('data-secret-id', secret.id);
deleteButton.disabled = Boolean(state.deleting[secret.id]) || state.modalSubmitting;
deleteButton.addEventListener('click', async function () {
await deleteSecret(secret);
});
@@ -378,6 +396,7 @@ function initSecretsPage() {
rotateButton.className = 'btn-secondary';
rotateButton.type = 'button';
rotateButton.textContent = tKey('secrets.action.rotate');
rotateButton.disabled = Boolean(state.deleting[secret.id]) || state.modalSubmitting;
rotateButton.addEventListener('click', function() {
openModal('rotate', secret);
});
@@ -387,6 +406,7 @@ function initSecretsPage() {
deleteButton.className = 'btn-secondary';
deleteButton.type = 'button';
deleteButton.textContent = tKey('secrets.action.delete');
deleteButton.disabled = Boolean(state.deleting[secret.id]) || state.modalSubmitting;
deleteButton.addEventListener('click', async function() {
await deleteSecret(secret);
});
@@ -412,12 +432,14 @@ function initSecretsPage() {
}
async function load() {
state.workspaceId = currentWorkspaceId();
var workspaceId = currentWorkspaceId();
var generation = ++state.generation;
state.workspaceId = workspaceId;
state.loading = true;
state.error = '';
renderSecrets();
if (!state.workspaceId) {
if (!workspaceId) {
state.secrets = [];
state.profiles = [];
recomputeDerivedState();
@@ -429,49 +451,79 @@ function initSecretsPage() {
try {
var results = await Promise.all([
window.CrankApi.listSecrets(state.workspaceId),
window.CrankApi.listAuthProfiles(state.workspaceId),
window.CrankApi.listSecrets(workspaceId),
window.CrankApi.listAuthProfiles(workspaceId),
]);
if (generation !== state.generation || workspaceId !== currentWorkspaceId()) {
return;
}
state.secrets = (results[0] && results[0].items) || [];
state.profiles = (results[1] && results[1].items) || [];
recomputeDerivedState();
} catch (error) {
if (generation !== state.generation || workspaceId !== currentWorkspaceId()) {
return;
}
state.error = error.message || tKey('secrets.error.load');
state.secrets = [];
state.profiles = [];
recomputeDerivedState();
} finally {
if (generation !== state.generation || workspaceId !== currentWorkspaceId()) {
return;
}
state.loading = false;
renderSecrets();
}
}
async function deleteSecret(secret) {
if (state.deleting[secret.id]) {
return;
}
if (!confirm(tfKey('secrets.confirm.delete', { name: secret.name }))) {
return;
}
var workspaceId = state.workspaceId;
state.deleting[secret.id] = true;
renderSecrets();
try {
await window.CrankApi.deleteSecret(state.workspaceId, secret.id);
await load();
if (window.CrankUi) {
await window.CrankApi.deleteSecret(workspaceId, secret.id);
if (workspaceId === currentWorkspaceId()) {
await load();
}
if (window.CrankUi && workspaceId === currentWorkspaceId()) {
window.CrankUi.success(
tfKey('secrets.toast.delete_message', { name: secret.name }),
tKey('secrets.toast.delete_title')
);
}
} catch (error) {
if (window.CrankUi) {
if (window.CrankUi && workspaceId === currentWorkspaceId()) {
window.CrankUi.error(
error.message || tKey('secrets.toast.delete_error_message'),
tKey('secrets.toast.delete_error_title')
);
}
} finally {
delete state.deleting[secret.id];
if (workspaceId === currentWorkspaceId()) {
renderSecrets();
}
}
}
async function submitModal() {
if (state.modalSubmitting) {
return;
}
var kind = modalKind.value;
var value = buildSecretValue();
var workspaceId = state.workspaceId;
var secretId = state.modalSecretId;
var submissionGeneration = ++state.modalGeneration;
var submissionMode = state.modalMode;
state.modalSubmitting = true;
modalSubmit.disabled = true;
modalSubmit.textContent = state.modalMode === 'rotate'
? tKey('secrets.modal.rotating')
@@ -479,7 +531,16 @@ function initSecretsPage() {
try {
if (state.modalMode === 'rotate') {
await window.CrankApi.rotateSecret(state.workspaceId, state.modalSecretId, { value: value });
await window.CrankApi.rotateSecret(workspaceId, secretId, { value: value });
if (
submissionGeneration !== state.modalGeneration
|| workspaceId !== currentWorkspaceId()
|| state.modalSecretId !== secretId
|| submissionMode !== state.modalMode
|| !modal.classList.contains('open')
) {
return;
}
if (window.CrankUi) {
window.CrankUi.success(
tKey('secrets.toast.rotate_message'),
@@ -491,11 +552,19 @@ function initSecretsPage() {
if (!name) {
throw new Error(tKey('secrets.validation.name_required'));
}
await window.CrankApi.createSecret(state.workspaceId, {
await window.CrankApi.createSecret(workspaceId, {
name: name,
kind: kind,
value: value,
});
if (
submissionGeneration !== state.modalGeneration
|| workspaceId !== currentWorkspaceId()
|| submissionMode !== state.modalMode
|| !modal.classList.contains('open')
) {
return;
}
if (window.CrankUi) {
window.CrankUi.success(
tKey('secrets.toast.create_message'),
@@ -506,17 +575,20 @@ function initSecretsPage() {
closeModal();
await load();
} catch (error) {
if (window.CrankUi) {
if (window.CrankUi && submissionGeneration === state.modalGeneration) {
window.CrankUi.error(
error.message || tKey(state.modalMode === 'rotate' ? 'secrets.toast.rotate_error_message' : 'secrets.toast.create_error_message'),
tKey(state.modalMode === 'rotate' ? 'secrets.toast.rotate_error_title' : 'secrets.toast.create_error_title')
);
}
} finally {
modalSubmit.disabled = false;
modalSubmit.textContent = state.modalMode === 'rotate'
? tKey('secrets.modal.rotate_action')
: tKey('secrets.modal.create_action');
if (submissionGeneration === state.modalGeneration) {
state.modalSubmitting = false;
modalSubmit.disabled = false;
modalSubmit.textContent = state.modalMode === 'rotate'
? tKey('secrets.modal.rotate_action')
: tKey('secrets.modal.create_action');
}
}
}
@@ -539,6 +611,8 @@ function initSecretsPage() {
renderSecrets();
});
window.addEventListener('crank:workspacechange', function () {
closeModal();
state.deleting = {};
void load();
});
+14 -2
View File
@@ -209,10 +209,22 @@ function bindPasswordSave() {
document.getElementById('security-current-password').value = '';
document.getElementById('security-new-password').value = '';
document.getElementById('security-confirm-password').value = '';
setStatus('settings-password-status', tKey('settings.security.saved'), false);
setStatus('settings-password-status', tKey('settings.security.saved_relogin'), false);
button.textContent = tKey('settings.profile.saved');
setTimeout(function() {
if (window.CrankAuth && typeof window.CrankAuth.handleUnauthorized === 'function') {
window.CrankAuth.handleUnauthorized();
}
}, 600);
} catch (error) {
setStatus('settings-password-status', error.message || tKey('settings.security.save_error'), true);
var diagnostics = [];
if (error && error.requestId) diagnostics.push('Request ID: ' + error.requestId);
if (error && error.traceId) diagnostics.push('Trace ID: ' + error.traceId);
setStatus(
'settings-password-status',
tKey('settings.security.save_error') + (diagnostics.length ? ('\n' + diagnostics.join('\n')) : ''),
true
);
button.textContent = original;
} finally {
setTimeout(function() {
+110 -32
View File
@@ -11,6 +11,8 @@ document.addEventListener('DOMContentLoaded', function () {
},
loading: false,
loadError: '',
usageEpoch: 0,
exporting: false,
};
var periodSelect = document.getElementById('period');
@@ -20,6 +22,7 @@ document.addEventListener('DOMContentLoaded', function () {
var cardList = document.getElementById('usage-card-list');
var chartTemplate = document.getElementById('tmpl-chart-bar');
var rowTemplate = document.getElementById('tmpl-usage-row');
var outcomeList = document.getElementById('usage-outcome-list');
var subtitle = document.querySelector('.section-card-subtitle');
var statCards = document.querySelectorAll('.stats-grid .stat-card');
@@ -73,6 +76,12 @@ document.addEventListener('DOMContentLoaded', function () {
return 'REST';
}
function outcomeLabel(outcome) {
var key = 'usage.outcome.' + outcome;
var translated = tKey(key);
return translated === key ? outcome : translated;
}
function localizedUsageOperation(operation) {
if (!window.localizeDemoOperation) {
return operation;
@@ -439,6 +448,43 @@ document.addEventListener('DOMContentLoaded', function () {
return item;
}
function renderOutcomes() {
if (!outcomeList) {
return;
}
outcomeList.innerHTML = '';
var outcomes = state.usage ? (state.usage.outcomes || []) : [];
if (!outcomes.length) {
outcomeList.appendChild(buildEmptyState(
tKey('usage.outcomes.empty.title'),
tKey('usage.outcomes.empty.sub'),
true
));
return;
}
outcomes.forEach(function (outcome) {
var card = element('div', 'resource-card');
var header = element('div', 'resource-card-header');
var headerMain = element('div');
headerMain.appendChild(element('div', 'resource-card-title', outcomeLabel(outcome.group)));
headerMain.appendChild(element(
'div',
'resource-card-subtitle',
outcome.execution_error_code || tKey('usage.outcome.no_error_code')
));
header.appendChild(headerMain);
header.appendChild(element('span', 'badge', formatCount(outcome.calls_total)));
card.appendChild(header);
var metaGrid = element('div', 'resource-meta-grid');
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.calls', formatCount(outcome.calls_total)));
metaGrid.appendChild(buildUsageMetaItem('usage.outcomes.p50', formatMs(outcome.p50_ms)));
metaGrid.appendChild(buildUsageMetaItem('usage.outcomes.p95', formatMs(outcome.p95_ms)));
metaGrid.appendChild(buildUsageMetaItem('usage.outcomes.p99', formatMs(outcome.p99_ms)));
card.appendChild(metaGrid);
outcomeList.appendChild(card);
});
}
function renderUsage() {
if (state.loading && !state.usage) {
renderEmpty(tKey('usage.loading.title'), tKey('usage.loading.sub'));
@@ -452,6 +498,7 @@ document.addEventListener('DOMContentLoaded', function () {
renderStats();
renderChart();
renderOutcomes();
renderTable();
if (subtitle) {
subtitle.textContent = tfKey('usage.table.subtitle', { period: periodLabel(state.period) });
@@ -472,24 +519,46 @@ document.addEventListener('DOMContentLoaded', function () {
return;
}
var workspaceId = state.workspaceId;
var epoch = ++state.usageEpoch;
state.loading = true;
state.loadError = '';
renderUsage();
try {
state.usage = await window.CrankApi.getUsageOverview(state.workspaceId, { period: state.period });
var usage = await window.CrankApi.getUsageOverview(workspaceId, { period: state.period });
if (epoch !== state.usageEpoch || workspaceId !== currentWorkspaceId()) {
return;
}
state.usage = usage;
recomputeDerivedState();
} catch (error) {
if (epoch !== state.usageEpoch) {
return;
}
state.loadError = error.message || tKey('usage.error.load');
state.usage = null;
recomputeDerivedState();
} finally {
state.loading = false;
renderUsage();
if (epoch === state.usageEpoch) {
state.loading = false;
renderUsage();
}
}
}
function exportCsv() {
function csvCell(value) {
var text = String(value === null || value === undefined ? '' : value);
if (/^[=+\-@\t\r\n]/.test(text)) {
text = "'" + text;
}
return '"' + text.replace(/"/g, '""') + '"';
}
async function exportCsv() {
if (!window.CrankApi || state.exporting) {
return;
}
if (!state.usage) {
if (window.CrankUi) {
window.CrankUi.info(tKey('usage.export.empty.body'), tKey('usage.export.empty.title'));
@@ -497,33 +566,39 @@ document.addEventListener('DOMContentLoaded', function () {
return;
}
var rows = [tKey('usage.csv.header')];
state.usage.operations.forEach(function (operation) {
var localizedOperation = localizedUsageOperation(operation);
var errorRate = operation.calls_total === 0
? 0
: ((operation.calls_error / operation.calls_total) * 100);
rows.push([
'"' + (localizedOperation.operation_display_name || operation.operation_display_name) + '"',
'"' + protocolLabel(operation.protocol) + '"',
operation.calls_total,
operation.calls_error,
errorRate.toFixed(2),
operation.p50_ms,
operation.p95_ms,
operation.p99_ms,
].join(','));
});
var blob = new Blob([rows.join('\r\n')], { type: 'text/csv' });
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = 'crank-usage-' + state.period + '.csv';
link.click();
URL.revokeObjectURL(url);
if (window.CrankUi) {
window.CrankUi.success(tKey('usage.export.done.body'), tKey('usage.export.done.title'));
var workspaceId = currentWorkspaceId();
if (!workspaceId) {
return;
}
var epoch = state.usageEpoch;
state.exporting = true;
if (exportBtn) {
exportBtn.disabled = true;
}
try {
var csv = await window.CrankApi.exportUsageCsv(workspaceId, { period: state.period });
if (epoch !== state.usageEpoch || workspaceId !== currentWorkspaceId()) {
return;
}
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = 'crank-usage-' + state.period + '.csv';
link.click();
URL.revokeObjectURL(url);
if (window.CrankUi) {
window.CrankUi.success(tKey('usage.export.done.body'), tKey('usage.export.done.title'));
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('usage.export.error.body'), tKey('usage.export.error.title'));
}
} finally {
state.exporting = false;
if (exportBtn) {
exportBtn.disabled = false;
}
}
}
@@ -539,7 +614,10 @@ document.addEventListener('DOMContentLoaded', function () {
exportBtn.addEventListener('click', exportCsv);
}
window.addEventListener('crank:workspacechange', loadUsage);
window.addEventListener('crank:workspacechange', function () {
state.usageEpoch += 1;
loadUsage();
});
if (window.whenWorkspacesReady) {
window.whenWorkspacesReady().finally(loadUsage);
+111 -10
View File
@@ -1,3 +1,25 @@
var wizardOperationLoadGeneration = 0;
var wizardLifecycleActionBusy = false;
var wizardTestConfirmationToken = null;
function operationErrorCode(error) {
return error && error.payload && error.payload.error
? error.payload.error.code
: '';
}
function showStaleOperationError(error) {
if (operationErrorCode(error) !== 'operation_stale_version'
&& operationErrorCode(error) !== 'operation_precondition_required') {
return false;
}
showWizardLiveStatus(tKey('wizard.stale.title'), tKey('wizard.stale.body'), true);
if (window.CrankUi) {
window.CrankUi.error(tKey('wizard.stale.body'), tKey('wizard.stale.title'));
}
return true;
}
function buildToolDescription() {
var snapshot = wizardCurrentVersion && wizardCurrentVersion.snapshot
? wizardCurrentVersion.snapshot
@@ -98,6 +120,7 @@ async function saveOperation(stayOnPage) {
try {
await persistCurrentDraft(stayOnPage);
} catch (error) {
if (showStaleOperationError(error)) return;
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('wizard.error.save'), tKey('wizard.error.save_title'));
}
@@ -106,14 +129,23 @@ async function saveOperation(stayOnPage) {
async function loadOperationForEdit() {
if (!wizardWorkspaceId || !wizardEditId) return;
var detail = await window.CrankApi.getOperation(wizardWorkspaceId, wizardEditId);
var generation = ++wizardOperationLoadGeneration;
var requestedWorkspaceId = wizardWorkspaceId;
var requestedOperationId = wizardEditId;
var detail = await window.CrankApi.getOperation(requestedWorkspaceId, requestedOperationId);
if (generation !== wizardOperationLoadGeneration
|| requestedWorkspaceId !== wizardWorkspaceId
|| requestedOperationId !== wizardEditId) return;
wizardProtocol = detail.protocol || 'rest';
await loadWizardPanels([3]);
var draftVersion = await window.CrankApi.getOperationVersion(
wizardWorkspaceId,
wizardEditId,
requestedWorkspaceId,
requestedOperationId,
detail.draft_version_ref.version
);
if (generation !== wizardOperationLoadGeneration
|| requestedWorkspaceId !== wizardWorkspaceId
|| requestedOperationId !== wizardEditId) return;
wizardCurrentOperation = detail;
wizardCurrentVersion = draftVersion;
prefillWizardFromEdit(detail, draftVersion);
@@ -150,6 +182,8 @@ function bindWizardLiveActions() {
bindLiveAction('wizard-run-test', tKey('wizard.busy.test'), runWizardTest);
bindLiveAction('wizard-run-quality', tKey('wizard.busy.quality'), analyzeWizardQuality);
bindClick('wizard-copy-test-response', copyTestResponseToOutputSample);
bindClick('wizard-copy-request-id', function() { copyCorrelationId('wizard-test-request-id'); });
bindClick('wizard-copy-trace-id', function() { copyCorrelationId('wizard-test-trace-id'); });
bindClick('wizard-copy-agent-preview', copyAgentFacingPreview);
bindLiveAction('wizard-export-yaml', tKey('wizard.busy.export_yaml'), exportWizardYaml);
bindLiveAction('wizard-import-yaml', tKey('wizard.busy.import_yaml'), importWizardYaml);
@@ -180,6 +214,7 @@ function bindClick(id, handler) {
function bindLiveAction(id, busyLabel, handler) {
var element = document.getElementById(id);
if (!element) return;
element.dataset.lifecycleAction = 'true';
element.addEventListener('click', function(event) {
event.preventDefault();
runWizardLiveAction(element, busyLabel, handler);
@@ -233,11 +268,17 @@ function bindApprovalPolicyControls() {
}
async function runWizardLiveAction(button, busyLabel, handler) {
if (!button || button.dataset.busy === 'true') {
if (!button || button.dataset.busy === 'true' || wizardLifecycleActionBusy) {
return;
}
var originalLabel = button.textContent;
wizardLifecycleActionBusy = true;
var lifecycleButtons = Array.from(document.querySelectorAll('[data-lifecycle-action="true"]'));
lifecycleButtons.forEach(function(element) {
element.disabled = true;
element.setAttribute('aria-busy', 'true');
});
button.dataset.busy = 'true';
button.disabled = true;
button.classList.add('is-busy');
@@ -246,6 +287,7 @@ async function runWizardLiveAction(button, busyLabel, handler) {
try {
await handler();
} catch (error) {
if (showStaleOperationError(error)) return;
showWizardLiveStatus(
tKey('wizard.live.failed_title'),
error && error.message ? error.message : tKey('wizard.live.failed_body'),
@@ -258,8 +300,12 @@ async function runWizardLiveAction(button, busyLabel, handler) {
);
}
} finally {
wizardLifecycleActionBusy = false;
lifecycleButtons.forEach(function(element) {
element.disabled = false;
element.removeAttribute('aria-busy');
});
button.dataset.busy = 'false';
button.disabled = false;
button.classList.remove('is-busy');
button.textContent = originalLabel;
}
@@ -515,6 +561,15 @@ function copyAgentFacingPreview() {
}
}
function copyCorrelationId(elementId) {
var element = document.getElementById(elementId);
var value = element ? element.textContent : '';
if (!value || !navigator.clipboard || !navigator.clipboard.writeText) return;
navigator.clipboard.writeText(value).then(function() {
showWizardLiveStatus(tKey('wizard.test.id_copied'), tKey('wizard.test.id_copied_body'));
});
}
function severityLabel(severity) {
if (severity === 'error') return tKey('wizard.quality.severity_error');
if (severity === 'warning') return tKey('wizard.quality.severity_warning');
@@ -637,7 +692,7 @@ function renderImportQualityFindings(versionDocument) {
var empty = document.getElementById('wizard-quality-empty');
if (empty) {
empty.textContent = 'Рекомендации из OpenAPI import. Запустите проверку качества, чтобы пересчитать их по текущему черновику.';
empty.textContent = tKey('wizard.quality.import_findings_hint');
empty.hidden = false;
}
}
@@ -679,7 +734,12 @@ async function importWizardYaml() {
showWizardLiveStatus(tKey('wizard.yaml.none'), tKey('wizard.yaml.none_body'), true);
return;
}
var imported = await window.CrankApi.importOperation(wizardWorkspaceId, yamlDocument, 'upsert');
var imported = await window.CrankApi.importOperation(
wizardWorkspaceId,
yamlDocument,
'upsert',
wizardEditId
);
if (Array.isArray(imported.warnings) && imported.warnings.length > 0) {
sessionStorage.setItem('crank_import_guidance', JSON.stringify(imported.warnings));
}
@@ -688,6 +748,7 @@ async function importWizardYaml() {
}
async function publishWizardOperation() {
if (!confirm(tKey('wizard.publish.confirm'))) return;
var report = await analyzeWizardQuality();
if (report.blocking) {
throw new Error(tKey('wizard.quality.blocking_error'));
@@ -703,6 +764,7 @@ async function publishWizardOperation() {
tKey('wizard.publish.done'),
tfKey('wizard.publish.done_body', { version: published.published_version })
);
if (window.CrankOnboarding) window.CrankOnboarding.signalRefresh();
}
function handleImportYamlFileSelection(event) {
@@ -724,6 +786,17 @@ function describeWizardTestResult(result) {
};
}
function localizeWizardTestErrors(errors) {
return (Array.isArray(errors) ? errors : []).map(function(error) {
if (!error || !error.code) return error;
var key = 'execution.error.' + error.code;
var localized = tKey(key);
return Object.assign({}, error, {
message: localized === key ? error.message : localized,
});
});
}
async function uploadInputSampleFromWizard() {
await persistCurrentDraft(true);
await window.CrankApi.uploadInputSample(
@@ -759,21 +832,49 @@ async function generateDraftFromWizard() {
}
async function runWizardTest() {
var generation = wizardOperationLoadGeneration;
var requestedWorkspaceId = wizardWorkspaceId;
var requestedOperationId = wizardEditId;
await persistCurrentDraft(true);
var result = await window.CrankApi.runOperationTest(wizardWorkspaceId, wizardEditId, {
if (generation !== wizardOperationLoadGeneration
|| requestedWorkspaceId !== wizardWorkspaceId
|| requestedOperationId !== wizardEditId) return;
var result = await window.CrankApi.runOperationTest(requestedWorkspaceId, requestedOperationId, {
version: currentDraftVersion(),
input: parseStructuredText(textValue('wizard-test-input')),
confirmation_token: wizardTestConfirmationToken,
locale: localStorage.getItem('crank_lang') || 'en',
});
if (generation !== wizardOperationLoadGeneration
|| requestedWorkspaceId !== wizardWorkspaceId
|| requestedOperationId !== wizardEditId) return;
var confirmationError = Array.isArray(result.errors)
? result.errors.find(function(error) { return error && error.code === 'confirmation_required'; })
: null;
wizardTestConfirmationToken = confirmationError && confirmationError.context
? confirmationError.context.confirmation_token || null
: null;
wizardTestResponsePreview = result.response_preview;
setTextareaValue('wizard-test-request-preview', result.request_preview);
setTextareaValue('wizard-test-response-preview', result.response_preview);
setTextareaValue('wizard-test-errors', result.errors && result.errors.length ? result.errors : []);
setTextareaValue('wizard-test-errors', localizeWizardTestErrors(result.errors));
var status = describeWizardTestResult(result);
var requestId = document.getElementById('wizard-test-request-id');
var traceId = document.getElementById('wizard-test-trace-id');
var correlationRoot = document.getElementById('wizard-test-correlation');
if (requestId) requestId.textContent = result.request_id || '';
if (traceId) traceId.textContent = result.trace_id || '';
if (correlationRoot) correlationRoot.hidden = !(result.request_id && result.trace_id);
var correlation = tfKey('wizard.test.correlation', {
requestId: result.request_id || '—',
traceId: result.trace_id || '—'
});
showWizardLiveStatus(
status.title,
status.body,
status.body + '\n' + correlation,
status.isError
);
if (window.CrankOnboarding) window.CrankOnboarding.signalRefresh();
}
function copyTestResponseToOutputSample() {
-2
View File
@@ -213,8 +213,6 @@ function parseExecutionConfig(text) {
retry_policy: retry && retry.max_attempts ? { max_attempts: Number(retry.max_attempts) } : null,
auth_profile_ref: authProfileRef,
headers: headers,
protocol_options: null,
streaming: null,
};
return config;
+13 -6
View File
@@ -35,6 +35,9 @@ var selectedUpstreamId = window.selectedUpstreamId;
var editingUpstreamId = window.editingUpstreamId;
async function initWizardPage() {
var initialParams = new URLSearchParams(window.location.search);
var returnContext = initialParams.get('return') || '';
if (!/^\/[A-Za-z0-9/_?&=.%~-]{0,512}$/.test(returnContext)) returnContext = '';
renderSidebarBrand('create');
document.querySelector('.btn-continue').addEventListener('click', function() {
currentStep = window.currentStep || 1;
@@ -57,14 +60,14 @@ async function initWizardPage() {
var backToCatalog = document.getElementById('back-to-catalog');
if (backToCatalog) {
backToCatalog.addEventListener('click', function() {
window.location.href = (window.CrankRoutes && window.CrankRoutes.home) || '/';
window.location.href = returnContext || ((window.CrankRoutes && window.CrankRoutes.home) || '/');
});
}
var closeBtn = document.querySelector('.progress-close');
if (closeBtn) {
closeBtn.addEventListener('click', function() {
window.location.href = (window.CrankRoutes && window.CrankRoutes.home) || '/';
window.location.href = returnContext || ((window.CrankRoutes && window.CrankRoutes.home) || '/');
});
}
@@ -132,10 +135,11 @@ async function initWizardPage() {
});
}
var params = new URLSearchParams(window.location.search);
if (params.get('mode') === 'edit' && params.get('operationId')) {
var params = initialParams;
var requestedOperationId = params.get('operationId') || '';
if (params.get('mode') === 'edit' && /^[A-Za-z0-9_-]{1,128}$/.test(requestedOperationId)) {
wizardMode = 'edit';
wizardEditId = params.get('operationId');
wizardEditId = requestedOperationId;
window.wizardMode = wizardMode;
window.wizardEditId = wizardEditId;
document.title = 'Crank — ' + tKey('wizard.progress.edit');
@@ -144,7 +148,10 @@ async function initWizardPage() {
}
updateWizardProtocolVisibility();
_doGoToStep(1);
var requestedStep = Number(params.get('step') || 1);
_doGoToStep(Number.isInteger(requestedStep) && requestedStep >= 1 && requestedStep <= TOTAL_STEPS ? requestedStep : 1);
window.CrankWizardReady = true;
document.dispatchEvent(new CustomEvent('crank:wizard-ready'));
}
function renderEditionCapabilityHints(capabilities) {