feat: complete Epic 1 production foundation
This commit is contained in:
+123
-18
@@ -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
@@ -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
@@ -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));
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user