chore: publish clean community baseline
Deploy / deploy (push) Successful in 2m32s
CI / Rust Checks (push) Successful in 5m33s
CI / UI Checks (push) Successful in 6s
CI / Deployment Manifests (push) Successful in 2s
CI / Frontend E2E (push) Successful in 4m36s

This commit is contained in:
github-ops
2026-06-17 06:15:46 +00:00
commit ae09eeba30
318 changed files with 73473 additions and 0 deletions
+499
View File
@@ -0,0 +1,499 @@
function agentUiStatus(status) {
if (status === 'published') return 'published';
if (status === 'archived') return 'archived';
return status || 'draft';
}
function mapAgent(agent) {
var mapped = {
id: agent.id,
slug: agent.slug,
display_name: agent.display_name,
description: agent.description || '',
status: agentUiStatus(agent.status),
raw_status: agent.status,
operation_count: agent.operation_count || 0,
operation_ids: agent.operation_ids || [],
key_count: agent.key_count || 0,
calls_today: agent.calls_today || 0,
created_at: agent.created_at,
current_draft_version: agent.current_draft_version || 1,
latest_published_version: agent.latest_published_version,
mcp_endpoint: agent.mcp_endpoint || '',
};
return window.localizeDemoAgent ? window.localizeDemoAgent(mapped) : mapped;
}
function mapOperation(operation) {
var mapped = {
id: operation.id,
name: operation.name,
display_name: operation.display_name,
protocol: operation.protocol,
status: operation.status,
current_draft_version: operation.current_draft_version || 1,
latest_published_version: operation.latest_published_version,
};
return window.localizeDemoOperation ? window.localizeDemoOperation(mapped) : mapped;
}
function currentLocale() {
return localStorage.getItem('crank_lang') === 'ru' ? 'ru-RU' : 'en-US';
}
document.addEventListener('alpine:init', function() {
Alpine.data('agents', function() {
return {
agents: [],
operations: [],
capabilities: null,
loading: true,
loadError: '',
workspaceId: null,
agentSearch: '',
openDropdown: null,
drawerOpen: false,
drawerMode: 'create',
editingId: null,
saving: false,
form: {
display_name: '',
slug: '',
description: '',
status: 'published',
selectedOps: [],
},
opSearch: '',
slugManuallyEdited: false,
async init() {
var self = this;
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
this.workspaceId = workspace ? workspace.id : null;
await this.loadCapabilities();
await this.reload();
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && self.drawerOpen) {
self.closeDrawer();
}
});
document.addEventListener('click', function() {
self.openDropdown = null;
});
window.addEventListener('crank:workspacechange', async function(event) {
self.workspaceId = event.detail ? event.detail.id : null;
await self.loadCapabilities();
await self.reload();
});
},
async loadCapabilities() {
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
this.capabilities = null;
return;
}
try {
this.capabilities = await window.CrankApi.getCapabilities();
} catch (_error) {
this.capabilities = null;
}
},
async reload() {
this.loading = true;
this.loadError = '';
if (!this.workspaceId || !window.CrankApi) {
this.agents = [];
this.operations = [];
this.loading = false;
this.loadError = this.tKey('agents.error.api');
return;
}
try {
var responses = await Promise.all([
window.CrankApi.listAgents(this.workspaceId),
window.CrankApi.listOperations(this.workspaceId),
]);
this.agents = ((responses[0] && responses[0].items) || []).map(mapAgent);
this.operations = ((responses[1] && responses[1].items) || []).map(mapOperation);
} catch (error) {
this.agents = [];
this.operations = [];
this.loadError = error.message || this.tKey('agents.error.load');
}
this.loading = false;
},
get filteredAgents() {
var query = this.agentSearch.toLowerCase().trim();
if (!query) return this.agents;
return this.agents.filter(function(agent) {
return agent.display_name.toLowerCase().includes(query)
|| agent.slug.toLowerCase().includes(query)
|| (agent.description || '').toLowerCase().includes(query);
});
},
get totalCalls() {
return this.agents.reduce(function(sum, agent) {
return sum + (agent.calls_today || 0);
}, 0);
},
get activeCount() {
return this.agents.filter(function(agent) {
return agent.status === 'published';
}).length;
},
get totalOpsExposed() {
return this.agents.reduce(function(sum, agent) {
return sum + (agent.operation_count || 0);
}, 0);
},
get isCommunityBuild() {
return !!(this.capabilities && this.capabilities.edition === 'community');
},
agentCalloutText() {
if (this.isCommunityBuild) {
return this.tfKey('agents.callout.community', { count: this.operations.length });
}
return this.tfKey('agents.callout.default', { count: this.operations.length });
},
subtitleText() {
return this.tKey(this.isCommunityBuild ? 'agents.subtitle.community' : 'agents.subtitle');
},
emptyStateText() {
return this.tKey(this.isCommunityBuild ? 'agents.empty.initial.sub_community' : 'agents.empty.initial.sub');
},
drawerSubText() {
if (this.drawerMode === 'edit') {
return this.tKey('agents.drawer.edit_sub');
}
return this.tKey(this.isCommunityBuild ? 'agents.drawer.new_sub_community' : 'agents.drawer.new_sub');
},
operationsSubText() {
return this.tKey(this.isCommunityBuild ? 'agents.drawer.operations_sub_community' : 'agents.drawer.operations_sub');
},
get filteredOps() {
var query = this.opSearch.toLowerCase().trim();
if (!query) return this.operations;
return this.operations.filter(function(operation) {
return operation.name.toLowerCase().includes(query)
|| operation.display_name.toLowerCase().includes(query);
});
},
openCreate() {
this.drawerMode = 'create';
this.editingId = null;
this.form = {
display_name: '',
slug: '',
description: '',
status: 'published',
selectedOps: [],
};
this.opSearch = '';
this.slugManuallyEdited = false;
this.drawerOpen = true;
},
openEdit(agent) {
this.drawerMode = 'edit';
this.editingId = agent.id;
this.form = {
display_name: agent.display_name,
slug: agent.slug,
description: agent.description,
status: agent.raw_status || agent.status || 'draft',
selectedOps: [].concat(agent.operation_ids || []),
};
this.opSearch = '';
this.slugManuallyEdited = true;
this.drawerOpen = true;
},
closeDrawer() {
this.drawerOpen = false;
this.saving = false;
},
onNameInput(value) {
this.form.display_name = value;
if (!this.slugManuallyEdited) {
this.form.slug = value
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '');
}
},
onSlugInput(value) {
this.form.slug = value.toLowerCase().replace(/[^a-z0-9-]/g, '');
this.slugManuallyEdited = true;
},
toggleOp(operationId) {
var index = this.form.selectedOps.indexOf(operationId);
if (index === -1) {
this.form.selectedOps.push(operationId);
} else {
this.form.selectedOps.splice(index, 1);
}
},
isOpSelected(operationId) {
return this.form.selectedOps.includes(operationId);
},
async saveAgent() {
var self = this;
if (
this.saving
|| !this.workspaceId
|| !this.form.display_name.trim()
|| !this.form.slug.trim()
) {
return;
}
this.saving = true;
try {
var agentId = this.editingId;
var currentVersion = 1;
var existingAgent = this.drawerMode === 'edit'
? this.agents.find(function(item) { return item.id === agentId; })
: null;
var previousStatus = existingAgent ? (existingAgent.raw_status || 'draft') : 'draft';
if (this.drawerMode === 'create') {
var created = await window.CrankApi.createAgent(this.workspaceId, {
slug: this.form.slug,
display_name: this.form.display_name,
description: this.form.description,
instructions: {},
tool_selection_policy: {},
});
agentId = created.agent_id;
currentVersion = created.version || 1;
} else {
await window.CrankApi.updateAgent(this.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);
currentVersion = agent.current_draft_version || 1;
}
await window.CrankApi.saveAgentBindings(
this.workspaceId,
agentId,
this.form.selectedOps.map(function(operationId) {
var operation = self.operations.find(function(item) {
return item.id === operationId;
});
return {
operation_id: operationId,
operation_version: operation && operation.latest_published_version
? operation.latest_published_version
: operation && operation.current_draft_version
? operation.current_draft_version
: 1,
tool_name: operation ? operation.name : operationId,
tool_title: operation ? (operation.display_name || operation.name) : operationId,
tool_description_override: null,
enabled: true,
};
}),
);
if (this.form.status === 'published') {
await window.CrankApi.publishAgent(this.workspaceId, agentId, {
version: currentVersion,
});
} else if (this.form.status === 'archived') {
await window.CrankApi.archiveAgent(this.workspaceId, agentId);
} else if (this.drawerMode === 'edit' && previousStatus !== 'draft') {
await window.CrankApi.unpublishAgent(this.workspaceId, agentId);
}
await this.reload();
if (window.CrankUi) {
window.CrankUi.success(
this.tfKey('agents.toast.saved_message', {
name: this.form.display_name,
count: this.form.selectedOps.length
}),
this.drawerMode === 'create'
? this.tKey('agents.toast.saved_title_create')
: this.tKey('agents.toast.saved_title_update')
);
}
this.closeDrawer();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || this.tKey('agents.toast.save_error_message'),
this.tKey('agents.toast.save_error_title')
);
}
this.saving = false;
}
},
async deleteAgent(id) {
if (!confirm(this.tKey('agents.toast.delete_confirm'))) return;
try {
var agent = this.agents.find(function(item) { return item.id === id; });
await window.CrankApi.deleteAgent(this.workspaceId, id);
await this.reload();
if (window.CrankUi) {
window.CrankUi.success(
this.tfKey('agents.toast.delete_message', { name: agent ? agent.display_name : '' }),
this.tKey('agents.toast.delete_title')
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || this.tKey('agents.toast.delete_error_message'),
this.tKey('agents.toast.delete_error_title')
);
}
}
},
async applyLifecycle(agent, action) {
if (!this.workspaceId || !window.CrankApi) {
return;
}
try {
if (action === 'publish') {
await window.CrankApi.publishAgent(this.workspaceId, agent.id, {
version: agent.current_draft_version || 1,
});
} else if (action === 'unpublish') {
await window.CrankApi.unpublishAgent(this.workspaceId, agent.id);
} else if (action === 'archive') {
await window.CrankApi.archiveAgent(this.workspaceId, agent.id);
}
await this.reload();
if (window.CrankUi) {
window.CrankUi.success(
action === 'publish'
? this.tfKey('agents.toast.lifecycle_publish', { name: agent.display_name })
: action === 'unpublish'
? this.tfKey('agents.toast.lifecycle_unpublish', { name: agent.display_name })
: this.tfKey('agents.toast.lifecycle_archive', { name: agent.display_name }),
this.tKey('agents.toast.lifecycle_title')
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || this.tKey('agents.toast.lifecycle_error_message'),
this.tKey('agents.toast.lifecycle_error_title')
);
}
}
},
lifecycleAction(agent) {
if (agent.raw_status === 'published') {
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: 'publish', label: this.tKey('agents.lifecycle.publish') };
},
lifecycleLabel(agent) {
if (agent.raw_status === 'published') return this.tKey('agents.lifecycle.published');
if (agent.raw_status === 'archived') return this.tKey('agents.lifecycle.archived');
return this.tKey('agents.lifecycle.draft');
},
mcpEndpoint(agent) {
if (agent.mcp_endpoint) return agent.mcp_endpoint;
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
var workspaceSlug = workspace ? workspace.slug : 'default';
return '/mcp/v1/' + workspaceSlug + '/' + agent.slug;
},
endpointHelpText(agent) {
return this.tfKey(
this.isCommunityBuild ? 'agents.card.endpoint_help_community' : 'agents.card.endpoint_help',
{ count: agent.key_count || 0 }
);
},
copyEndpoint(agent) {
var endpoint = this.mcpEndpoint(agent);
navigator.clipboard.writeText(endpoint).catch(function() {});
if (window.CrankUi) {
window.CrankUi.info(endpoint, this.tKey('agents.toast.endpoint_title'));
}
},
statusClass(status) {
return 'agent-status-badge agent-status-' + status;
},
protocolBadge(operation) {
return 'badge badge-rest';
},
protocolLabel(operation) {
return 'REST';
},
formatDate(dateStr) {
if (!dateStr) return '—';
return new Date(dateStr).toLocaleDateString(currentLocale(), {
month: 'short',
day: 'numeric',
year: 'numeric',
});
},
formatCalls(value) {
if (!value) return '0';
return value >= 1000 ? (value / 1000).toFixed(1) + 'k' : String(value);
},
tKey(key) {
return window.t ? t(key) : key;
},
tfKey(key, vars) {
return window.tf ? tf(key, vars) : this.tKey(key);
},
};
});
});
+620
View File
@@ -0,0 +1,620 @@
var KEYS = [];
var AGENTS = [];
var capabilities = null;
var currentWorkspaceId = null;
var currentAgentId = null;
var selectedScopes = new Set(['read']);
var search = '';
var SCOPES = ['read', 'write', 'deploy'];
var SCOPE_DESC = {
read: 'apikeys.scope.read',
write: 'apikeys.scope.write',
deploy: 'apikeys.scope.deploy',
};
function tKey(key) {
return window.t ? t(key) : key;
}
function tfKey(key, vars) {
return window.tf ? tf(key, vars) : tKey(key);
}
function capabilityLabel(key) {
return tKey('settings.capability.' + key);
}
function buildIconSvg(href, width, height) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', String(width));
svg.setAttribute('height', String(height));
var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', href);
svg.appendChild(use);
return svg;
}
function mapKeyRecord(record) {
var apiKey = record.api_key || {};
return {
id: apiKey.id,
agentId: apiKey.agent_id || null,
name: apiKey.name,
prefix: apiKey.prefix || '',
scopes: apiKey.scopes || [],
created: apiKey.created_at ? apiKey.created_at.slice(0, 10) : '—',
lastUsed: apiKey.last_used_at ? apiKey.last_used_at.slice(0, 10) : tKey('apikeys.last_used.never'),
status: apiKey.status || 'active',
};
}
function mapAgentRecord(record) {
return {
id: record.id,
slug: record.slug,
displayName: record.display_name || record.slug || record.id,
mcpEndpoint: record.mcp_endpoint || '',
};
}
function currentWorkspace() {
return window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
}
async function loadKeys() {
var workspace = currentWorkspace();
currentWorkspaceId = workspace ? workspace.id : null;
setTableLoading(true);
if (!currentWorkspaceId || !window.CrankApi) {
AGENTS = [];
KEYS = [];
currentAgentId = null;
renderAgentPicker();
setCreateButtonState();
setTableLoading(false);
renderTable(tKey('apikeys.error.api'));
return;
}
try {
var response = await window.CrankApi.listAgents(currentWorkspaceId);
AGENTS = ((response && response.items) || []).map(mapAgentRecord);
if (!AGENTS.length) {
currentAgentId = null;
KEYS = [];
renderAgentPicker();
setCreateButtonState();
setTableLoading(false);
renderTable();
return;
}
if (!currentAgentId || !AGENTS.some(function(agent) { return agent.id === currentAgentId; })) {
currentAgentId = AGENTS[0].id;
}
renderAgentPicker();
var keysResponse = await window.CrankApi.listAgentPlatformApiKeys(
currentWorkspaceId,
currentAgentId
);
KEYS = ((keysResponse && keysResponse.items) || []).map(mapKeyRecord);
setCreateButtonState();
setTableLoading(false);
renderTable();
} catch (error) {
AGENTS = [];
KEYS = [];
currentAgentId = null;
renderAgentPicker();
setCreateButtonState();
setTableLoading(false);
renderTable(error.message || tKey('apikeys.error.load'));
}
}
async function loadCapabilities() {
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
return;
}
try {
capabilities = await window.CrankApi.getCapabilities();
} catch (_error) {
capabilities = null;
}
renderMachineAccessSummary();
}
function renderMachineAccessSummary() {
var title = document.getElementById('machine-access-title');
var subtitle = document.getElementById('machine-access-subtitle');
var summary = document.getElementById('machine-access-summary');
var note = document.getElementById('machine-access-note');
if (!title || !subtitle || !summary || !note) {
return;
}
if (!capabilities) {
summary.textContent = tKey('apikeys.machine_access.summary_default');
note.textContent = tKey('apikeys.machine_access.note');
return;
}
var accessModes = (capabilities.machine_access_modes || []).map(capabilityLabel).join(', ');
var securityLevels = (capabilities.supported_security_levels || []).map(capabilityLabel).join(', ');
title.textContent = tKey('apikeys.machine_access.title_' + capabilities.edition);
subtitle.textContent = tKey('apikeys.machine_access.subtitle_' + capabilities.edition);
summary.textContent = tfKey('apikeys.machine_access.summary', {
access_modes: accessModes || capabilityLabel('none'),
security_levels: securityLevels || capabilityLabel('none'),
});
note.textContent = tKey('apikeys.machine_access.note_' + capabilities.edition);
}
function setTableLoading(on) {
var tbody = document.getElementById('keys-tbody');
if (!on) return;
tbody.innerHTML = '';
var tr = document.createElement('tr');
var td = document.createElement('td');
td.colSpan = 7;
td.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
td.textContent = tKey('apikeys.loading');
tr.appendChild(td);
tbody.appendChild(tr);
}
async function revokeKey(id) {
if (!confirm(tKey('apikeys.confirm.revoke'))) return;
if (!currentAgentId) return;
try {
var key = KEYS.find(function(item) { return item.id === id; });
await window.CrankApi.revokeAgentPlatformApiKey(currentWorkspaceId, currentAgentId, id);
await loadKeys();
if (window.CrankUi) {
window.CrankUi.success(
tfKey('apikeys.toast.revoke_message', { name: key ? key.name : '' }),
tKey('apikeys.toast.revoke_title')
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || tKey('apikeys.toast.revoke_error_message'),
tKey('apikeys.toast.revoke_error_title')
);
}
}
}
async function deleteKey(id) {
if (!confirm(tKey('apikeys.confirm.delete'))) return;
if (!currentAgentId) return;
try {
var key = KEYS.find(function(item) { return item.id === id; });
await window.CrankApi.deleteAgentPlatformApiKey(currentWorkspaceId, currentAgentId, id);
await loadKeys();
if (window.CrankUi) {
window.CrankUi.success(
tfKey('apikeys.toast.delete_message', { name: key ? key.name : '' }),
tKey('apikeys.toast.delete_title')
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || tKey('apikeys.toast.delete_error_message'),
tKey('apikeys.toast.delete_error_title')
);
}
}
}
async function createKey(name, scopes) {
var created = await window.CrankApi.createAgentPlatformApiKey(currentWorkspaceId, currentAgentId, {
name: name,
scopes: scopes,
});
return {
rawKey: created.secret,
record: mapKeyRecord(created.api_key),
};
}
function currentAgent() {
return AGENTS.find(function(agent) { return agent.id === currentAgentId; }) || null;
}
function renderAgentPicker() {
var select = document.getElementById('agent-select');
var hint = document.getElementById('agent-endpoint-hint');
if (!select || !hint) return;
select.innerHTML = '';
if (!AGENTS.length) {
var emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = tKey('apikeys.agent.empty');
select.appendChild(emptyOption);
select.disabled = true;
hint.textContent = tKey('apikeys.agent.empty_hint');
return;
}
AGENTS.forEach(function(agent) {
var option = document.createElement('option');
option.value = agent.id;
option.textContent = agent.displayName;
if (agent.id === currentAgentId) option.selected = true;
select.appendChild(option);
});
select.disabled = false;
var agent = currentAgent();
hint.textContent = agent && agent.mcpEndpoint
? tfKey('apikeys.agent.endpoint', { endpoint: agent.mcpEndpoint })
: tKey('apikeys.agent.endpoint_missing');
}
function setCreateButtonState() {
var button = document.getElementById('btn-create-key');
if (!button) return;
button.disabled = !currentAgentId;
}
function renderScopes() {
var el = document.getElementById('scope-checkboxes');
var tmpl = document.getElementById('tmpl-scope-checkbox');
el.innerHTML = '';
SCOPES.forEach(function(scope) {
var node = tmpl.content.cloneNode(true);
var input = node.querySelector('input');
input.dataset.scope = scope;
if (selectedScopes.has(scope)) input.checked = true;
node.querySelector('.scope-checkbox-name').textContent = scope;
node.querySelector('.scope-checkbox-desc').textContent = tKey(SCOPE_DESC[scope]);
input.addEventListener('change', function() {
if (this.checked) selectedScopes.add(this.dataset.scope);
else selectedScopes.delete(this.dataset.scope);
});
el.appendChild(node);
});
}
function renderTable(errorMessage) {
var q = search.toLowerCase();
var tbody = document.getElementById('keys-tbody');
var cardList = document.getElementById('keys-card-list');
var rows = KEYS.filter(function(key) {
return !q || key.name.toLowerCase().includes(q) || key.prefix.toLowerCase().includes(q);
});
var subtitle = document.getElementById('keys-summary-subtitle');
tbody.innerHTML = '';
if (cardList) {
cardList.innerHTML = '';
}
if (subtitle) {
var active = KEYS.filter(function(key) { return key.status === 'active'; }).length;
var revoked = KEYS.filter(function(key) { return key.status === 'revoked'; }).length;
subtitle.textContent = currentAgentId
? tfKey('apikeys.active.subtitle', { active: active, revoked: revoked })
: tKey('apikeys.agent.empty_hint');
}
if (errorMessage) {
var errorRow = document.createElement('tr');
var errorCell = document.createElement('td');
errorCell.colSpan = 7;
errorCell.style.cssText = 'text-align:center;padding:36px;color:var(--danger,#f85149);';
errorCell.textContent = errorMessage;
errorRow.appendChild(errorCell);
tbody.appendChild(errorRow);
renderKeyCards([], errorMessage);
return;
}
if (!rows.length) {
var empty = document.createElement('tr');
var td = document.createElement('td');
td.colSpan = 7;
td.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
td.textContent = currentAgentId
? (KEYS.length ? tKey('apikeys.empty.search') : tKey('apikeys.empty.none'))
: tKey('apikeys.agent.empty_hint');
empty.appendChild(td);
tbody.appendChild(empty);
renderKeyCards(rows);
return;
}
var tmpl = document.getElementById('tmpl-key-row');
rows.forEach(function(key) {
var node = tmpl.content.cloneNode(true);
node.querySelector('.col-name').textContent = key.name;
node.querySelector('.col-mono').textContent = key.prefix;
node.querySelector('.col-created').textContent = key.created;
node.querySelector('.col-last-used').textContent = key.lastUsed;
var scopesEl = node.querySelector('.col-scopes');
key.scopes.forEach(function(scope) {
var span = document.createElement('span');
span.className = 'badge badge-scope';
span.textContent = scope;
scopesEl.appendChild(span);
});
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');
node.querySelector('.col-status').appendChild(badge);
var actionsActive = node.querySelector('.actions-active');
var actionsRevoked = node.querySelector('.actions-revoked');
if (key.status === 'active') {
actionsActive.querySelector('[title=\"Copy key prefix\"]').addEventListener('click', function() {
copyPrefix(key.prefix);
});
actionsActive.querySelector('[title=\"Revoke key\"]').addEventListener('click', function() {
revokeKey(key.id);
});
} else {
actionsActive.hidden = true;
actionsRevoked.hidden = false;
actionsRevoked.querySelector('[title=\"Delete\"]').addEventListener('click', function() {
deleteKey(key.id);
});
}
tbody.appendChild(node);
});
renderKeyCards(rows);
}
function renderKeyCards(rows, errorMessage) {
var cardList = document.getElementById('keys-card-list');
if (!cardList) return;
cardList.innerHTML = '';
if (errorMessage) {
cardList.appendChild(buildKeyCardMessage(errorMessage, true));
return;
}
if (!rows.length) {
cardList.appendChild(
buildKeyCardMessage(
currentAgentId
? (KEYS.length ? tKey('apikeys.empty.search') : tKey('apikeys.empty.none'))
: tKey('apikeys.agent.empty_hint'),
false
)
);
return;
}
rows.forEach(function(key) {
var card = document.createElement('div');
card.className = 'resource-card';
var header = document.createElement('div');
header.className = 'resource-card-header';
var headerMain = document.createElement('div');
var title = document.createElement('div');
title.className = 'resource-card-title';
title.textContent = key.name;
var subtitle = document.createElement('div');
subtitle.className = 'resource-card-subtitle';
subtitle.textContent = key.prefix;
headerMain.appendChild(title);
headerMain.appendChild(subtitle);
var actions = document.createElement('div');
actions.className = 'resource-card-actions';
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');
actions.appendChild(statusBadge);
header.appendChild(headerMain);
header.appendChild(actions);
card.appendChild(header);
var metaGrid = document.createElement('div');
metaGrid.className = 'resource-meta-grid';
metaGrid.appendChild(buildMetaItem('apikeys.th.scopes', key.scopes.join(', ') || '—'));
metaGrid.appendChild(buildMetaItem('apikeys.th.created', key.created));
metaGrid.appendChild(buildMetaItem('apikeys.th.last', key.lastUsed));
card.appendChild(metaGrid);
var actionRow = document.createElement('div');
actionRow.className = 'resource-card-actions';
if (key.status === 'active') {
actionRow.appendChild(buildCardAction('apikeys.action.copy_prefix', function() {
copyPrefix(key.prefix);
}));
actionRow.appendChild(buildCardAction('apikeys.action.revoke', function() {
revokeKey(key.id);
}, true));
} else {
actionRow.appendChild(buildCardAction('apikeys.action.delete', function() {
deleteKey(key.id);
}, true));
}
card.appendChild(actionRow);
cardList.appendChild(card);
});
}
function buildKeyCardMessage(text, isError) {
var card = document.createElement('div');
card.className = 'resource-card';
var value = document.createElement('div');
value.className = 'resource-meta-value';
value.textContent = text;
value.style.color = isError ? 'var(--danger,#f85149)' : 'var(--text-muted)';
card.appendChild(value);
return card;
}
function buildMetaItem(labelKey, valueText) {
var item = document.createElement('div');
item.className = 'resource-meta-item';
var label = document.createElement('div');
label.className = 'resource-meta-label';
label.textContent = tKey(labelKey);
var value = document.createElement('div');
value.className = 'resource-meta-value';
value.textContent = valueText;
item.appendChild(label);
item.appendChild(value);
return item;
}
function buildCardAction(labelKey, onClick, isDanger) {
var button = document.createElement('button');
button.type = 'button';
button.className = isDanger ? 'btn-secondary' : 'btn-secondary';
button.textContent = tKey(labelKey);
button.addEventListener('click', onClick);
return button;
}
var modal = document.getElementById('modal-create');
function openModal() {
if (!currentAgentId) return;
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(['read']);
renderScopes();
modal.classList.add('open');
setTimeout(function() {
document.getElementById('new-key-name').focus();
}, 50);
}
function closeModal() {
modal.classList.remove('open');
}
document.getElementById('btn-create-key').addEventListener('click', openModal);
document.getElementById('modal-close-btn').addEventListener('click', closeModal);
document.getElementById('modal-cancel-btn').addEventListener('click', closeModal);
modal.addEventListener('click', function(event) {
if (event.target === modal) closeModal();
});
document.getElementById('modal-confirm-btn').addEventListener('click', async function() {
var button = this;
var name = document.getElementById('new-key-name').value.trim();
if (!name) {
document.getElementById('new-key-name').focus();
return;
}
if (!selectedScopes.size) {
if (window.CrankUi) {
window.CrankUi.info(
tKey('apikeys.toast.scope_missing_message'),
tKey('apikeys.toast.scope_missing_title')
);
}
return;
}
try {
button.disabled = true;
button.textContent = tKey('apikeys.creating');
var created = await createKey(name, Array.from(selectedScopes));
KEYS.unshift(created.record);
document.getElementById('reveal-key-value').textContent = created.rawKey;
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.CrankUi) {
window.CrankUi.success(
tKey('apikeys.toast.create_message'),
tKey('apikeys.toast.create_title')
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || tKey('apikeys.toast.create_error_message'),
tKey('apikeys.toast.create_error_title')
);
}
} finally {
button.disabled = false;
button.textContent = 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() {});
}
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')
);
}
});
document.getElementById('key-search').addEventListener('input', function() {
search = this.value;
renderTable();
});
document.getElementById('agent-select').addEventListener('change', async function() {
currentAgentId = this.value || null;
await loadKeys();
});
function copyPrefix(prefix) {
if (navigator.clipboard) {
navigator.clipboard.writeText(prefix).catch(function() {});
}
if (window.CrankUi) {
window.CrankUi.info(prefix, tKey('apikeys.toast.prefix_title'));
}
}
document.addEventListener('DOMContentLoaded', async function() {
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
await loadCapabilities();
await loadKeys();
window.addEventListener('crank:workspacechange', function() {
loadCapabilities();
loadKeys();
});
});
+351
View File
@@ -0,0 +1,351 @@
(function() {
var API_BASE = '/api/admin';
var AUTH_BASE = '/api/auth';
function headers(extra) {
return Object.assign({
'Accept': 'application/json',
}, extra || {});
}
async function request(path, options) {
var response = await fetch(path, Object.assign({
credentials: 'same-origin',
headers: headers(),
}, options || {}));
if (response.status === 204) {
return null;
}
var payload = null;
var text = await response.text();
if (text) {
try {
payload = JSON.parse(text);
} catch (_error) {}
}
if (!response.ok) {
if (response.status === 401 && window.CrankAuth && typeof window.CrankAuth.handleUnauthorized === 'function') {
window.CrankAuth.handleUnauthorized();
}
var message = payload && payload.error
? payload.error.message
: payload && payload.message
? payload.message
: text
? text
: ('HTTP ' + response.status);
var error = new Error(message);
error.status = response.status;
error.payload = payload;
throw error;
}
return payload;
}
async function requestText(path, options) {
var response = await fetch(path, Object.assign({
credentials: 'same-origin',
headers: headers(),
}, options || {}));
var text = await response.text();
var payload = null;
if (text) {
try {
payload = JSON.parse(text);
} catch (_error) {}
}
if (!response.ok) {
if (response.status === 401 && window.CrankAuth && typeof window.CrankAuth.handleUnauthorized === 'function') {
window.CrankAuth.handleUnauthorized();
}
var message = payload && payload.error
? payload.error.message
: payload && payload.message
? payload.message
: text
? text
: ('HTTP ' + response.status);
var error = new Error(message);
error.status = response.status;
error.payload = payload;
throw error;
}
return text;
}
function get(path) {
return request(API_BASE + path);
}
function post(path, body) {
return request(API_BASE + path, {
method: 'POST',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(body),
});
}
function patch(path, body) {
return request(API_BASE + path, {
method: 'PATCH',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(body),
});
}
function del(path) {
return request(API_BASE + path, { method: 'DELETE' });
}
function query(params) {
var search = new URLSearchParams();
Object.keys(params || {}).forEach(function(key) {
var value = params[key];
if (value === undefined || value === null || value === '') {
return;
}
search.set(key, value);
});
var encoded = search.toString();
return encoded ? ('?' + encoded) : '';
}
function postBytes(path, bytes, fileName) {
return request(API_BASE + path, {
method: 'POST',
headers: headers(fileName ? { 'X-File-Name': fileName } : {}),
body: bytes,
});
}
window.CrankApi = {
login: function(payload) {
return request(AUTH_BASE + '/login', {
method: 'POST',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload),
});
},
logout: function() {
return request(AUTH_BASE + '/logout', {
method: 'POST',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify({}),
});
},
getSession: function() {
return request(AUTH_BASE + '/session');
},
getProfile: function() {
return request(AUTH_BASE + '/profile');
},
updateProfile: function(payload) {
return request(AUTH_BASE + '/profile', {
method: 'PATCH',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload),
});
},
changePassword: function(payload) {
return request(AUTH_BASE + '/password', {
method: 'POST',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload),
});
},
setCurrentWorkspace: function(workspaceId) {
return request(AUTH_BASE + '/current-workspace', {
method: 'POST',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ workspace_id: workspaceId }),
});
},
listWorkspaces: function() {
return get('/workspaces');
},
getCapabilities: function() {
return get('/capabilities');
},
createWorkspace: function(payload) {
return post('/workspaces', payload);
},
getWorkspace: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId));
},
updateWorkspace: function(workspaceId, payload) {
return patch('/workspaces/' + encodeURIComponent(workspaceId), payload);
},
listMemberships: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/members');
},
updateMembership: function(workspaceId, userId, payload) {
return patch('/workspaces/' + encodeURIComponent(workspaceId) + '/members/' + encodeURIComponent(userId), payload);
},
deleteMembership: function(workspaceId, userId) {
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/members/' + encodeURIComponent(userId));
},
listInvitations: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/invitations');
},
createInvitation: function(workspaceId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/invitations', payload);
},
deleteInvitation: function(workspaceId, invitationId) {
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/invitations/' + encodeURIComponent(invitationId));
},
exportWorkspace: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/export');
},
deleteWorkspace: function(workspaceId) {
return del('/workspaces/' + encodeURIComponent(workspaceId));
},
listOperations: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/operations');
},
getOperation: function(workspaceId, operationId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId));
},
getOperationVersion: function(workspaceId, operationId, version) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/versions/' + encodeURIComponent(version));
},
createOperation: function(workspaceId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations', payload);
},
updateOperation: function(workspaceId, operationId, payload) {
return patch('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId), payload);
},
deleteOperation: function(workspaceId, operationId) {
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId));
},
archiveOperation: function(workspaceId, operationId) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/archive', {});
},
publishOperation: function(workspaceId, operationId, version) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/publish', {
version: version,
});
},
createOperationVersion: function(workspaceId, operationId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/versions', payload);
},
runOperationTest: function(workspaceId, operationId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/test-runs', payload);
},
uploadInputSample: function(workspaceId, operationId, sample) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/samples/input-json', sample);
},
uploadOutputSample: function(workspaceId, operationId, sample) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/samples/output-json', sample);
},
generateDraft: function(workspaceId, operationId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/drafts/generate', payload || {});
},
exportOperation: function(workspaceId, operationId, params) {
return requestText(
API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/export' + query(params),
{
headers: headers({ 'Accept': 'application/yaml' }),
}
);
},
importOperation: function(workspaceId, yamlDocument, mode) {
return request(
API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/operations/import' + query({ mode: mode }),
{
method: 'POST',
headers: headers({ 'Content-Type': 'application/yaml' }),
body: yamlDocument,
}
);
},
listAgents: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/agents');
},
createAgent: function(workspaceId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents', payload);
},
getAgent: function(workspaceId, agentId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId));
},
updateAgent: function(workspaceId, agentId, payload) {
return patch('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId), payload);
},
deleteAgent: function(workspaceId, agentId) {
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId));
},
saveAgentBindings: function(workspaceId, agentId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/bindings', payload);
},
publishAgent: function(workspaceId, agentId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/publish', payload);
},
unpublishAgent: function(workspaceId, agentId) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/unpublish', {});
},
archiveAgent: function(workspaceId, agentId) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/archive', {});
},
listAgentPlatformApiKeys: function(workspaceId, agentId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/platform-api-keys');
},
createAgentPlatformApiKey: function(workspaceId, agentId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/platform-api-keys', payload);
},
revokeAgentPlatformApiKey: function(workspaceId, agentId, keyId) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/platform-api-keys/' + encodeURIComponent(keyId) + '/revoke', {});
},
deleteAgentPlatformApiKey: function(workspaceId, agentId, keyId) {
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/platform-api-keys/' + encodeURIComponent(keyId));
},
listSecrets: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets');
},
createSecret: function(workspaceId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets', payload);
},
getSecret: function(workspaceId, secretId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets/' + encodeURIComponent(secretId));
},
rotateSecret: function(workspaceId, secretId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets/' + encodeURIComponent(secretId) + '/rotate', payload);
},
deleteSecret: function(workspaceId, secretId) {
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets/' + encodeURIComponent(secretId));
},
listAuthProfiles: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/auth-profiles');
},
listLogs: function(workspaceId, params) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs' + query(params));
},
getLog: function(workspaceId, logId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs/' + encodeURIComponent(logId));
},
getUsageOverview: function(workspaceId, params) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/usage' + query(params));
},
getProtocolCapabilities: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/protocol-capabilities');
},
getOperationUsage: function(workspaceId, operationId, params) {
return get(
'/workspaces/' + encodeURIComponent(workspaceId) + '/usage/operations/' + encodeURIComponent(operationId) + query(params)
);
},
getAgentUsage: function(workspaceId, agentId, params) {
return get(
'/workspaces/' + encodeURIComponent(workspaceId) + '/usage/agents/' + encodeURIComponent(agentId) + query(params)
);
},
};
}());
+253
View File
@@ -0,0 +1,253 @@
(function() {
var STORAGE_KEY = 'crank_user';
var sessionCache = null;
var sessionPromise = null;
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : key;
}
function roleLabel(role) {
var normalized = String(role || 'viewer').toLowerCase();
if (normalized === 'operator') return tKey('workspace_setup.role.operator');
if (normalized === 'owner') return tKey('workspace_setup.role.owner');
if (normalized === 'admin') return tKey('workspace_setup.role.admin');
if (normalized === 'viewer') return tKey('workspace_setup.role.viewer');
return normalized.replace(/^\w/, function(char) { return char.toUpperCase(); });
}
function currentWorkspaceLabel() {
if (window.getCurrentWorkspace) {
var workspace = window.getCurrentWorkspace();
if (workspace && workspace.name) {
return workspace.name;
}
}
try {
return localStorage.getItem('crank_workspace_slug') || tKey('settings.nav.workspace');
} catch (_error) {
return tKey('settings.nav.workspace');
}
}
function isLoginPage() {
return /\/(?:html\/login\.html|login)\/?$/.test(window.location.pathname);
}
function loginUrl() {
return (window.CrankRoutes && window.CrankRoutes.login) || '/login';
}
function homeUrl() {
return (window.CrankRoutes && window.CrankRoutes.home) || '/';
}
function clearUserMirror() {
sessionCache = null;
sessionPromise = null;
try {
localStorage.removeItem(STORAGE_KEY);
} catch (_error) {}
renderShellIdentity(null);
}
function primaryMembership(session) {
if (!(session && session.memberships && session.memberships.length)) {
return null;
}
if (session.current_workspace_id) {
var currentMembership = session.memberships.find(function(item) {
return item.workspace && item.workspace.id === session.current_workspace_id;
});
if (currentMembership) {
return currentMembership;
}
}
if (window.getCurrentWorkspace) {
var currentWorkspace = window.getCurrentWorkspace();
if (currentWorkspace) {
var matched = session.memberships.find(function(item) {
return item.workspace && item.workspace.id === currentWorkspace.id;
});
if (matched) {
return matched;
}
}
}
return session.memberships[0];
}
function initials(displayName, email) {
var source = displayName || email || 'Crank';
return source
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map(function(part) { return part.charAt(0).toUpperCase(); })
.join('') || 'CR';
}
function persistUserMirror(session) {
var membership = primaryMembership(session);
var user = {
id: session.user.id,
name: session.user.display_name,
email: session.user.email,
role: membership ? roleLabel(membership.role) : tKey('workspace_setup.role.viewer'),
workspace: membership ? membership.workspace.slug : '',
workspaceId: membership ? membership.workspace.id : '',
initials: initials(session.user.display_name, session.user.email),
};
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(user));
} catch (_error) {}
}
function mirroredUser() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY));
} catch (_error) {
return null;
}
}
function renderShellIdentity(session) {
var membership = primaryMembership(session);
var fallbackUser = mirroredUser();
var displayName = session && session.user
? (session.user.display_name || session.user.email || 'Crank')
: (fallbackUser && fallbackUser.name) || 'Crank';
var email = session && session.user
? session.user.email
: fallbackUser && fallbackUser.email;
var role = membership
? roleLabel(membership.role)
: (fallbackUser && fallbackUser.role) || tKey('workspace_setup.role.viewer');
var workspace = membership && membership.workspace
? (membership.workspace.display_name || membership.workspace.slug || membership.workspace.id)
: currentWorkspaceLabel();
var avatarValue = initials(displayName, email);
document.querySelectorAll('.nav-avatar').forEach(function(element) {
element.textContent = avatarValue;
});
document.querySelectorAll('.user-dropdown-name').forEach(function(element) {
element.textContent = displayName;
});
document.querySelectorAll('.user-dropdown-role').forEach(function(element) {
element.textContent = role + ' · ' + workspace;
});
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.ensureShellTestIds === 'function') {
window.CrankDiagnostics.ensureShellTestIds();
}
}
function replaceSession(session) {
sessionCache = session;
persistUserMirror(session);
renderShellIdentity(session);
window.dispatchEvent(new CustomEvent('crank:sessionchange', { detail: session }));
return session;
}
async function fetchSession(force) {
if (!force && sessionCache) {
return sessionCache;
}
if (!force && sessionPromise) {
return sessionPromise;
}
sessionPromise = window.CrankApi.getSession()
.then(function(session) {
return replaceSession(session);
})
.catch(function(error) {
clearUserMirror();
throw error;
})
.finally(function() {
sessionPromise = null;
});
return sessionPromise;
}
async function guardProtectedPage() {
try {
return await fetchSession(false);
} catch (error) {
if (error && error.status === 401) {
window.location.replace(loginUrl());
return null;
}
throw error;
}
}
async function guardLoginPage() {
try {
await fetchSession(false);
window.location.replace(homeUrl());
} catch (error) {
if (!error || error.status !== 401) {
throw error;
}
}
}
async function login(email, password) {
var session = await window.CrankApi.login({
email: email,
password: password,
});
replaceSession(session);
window.location.href = homeUrl();
}
async function logout() {
try {
await window.CrankApi.logout();
} finally {
clearUserMirror();
window.location.href = loginUrl();
}
}
function handleUnauthorized() {
if (isLoginPage()) {
clearUserMirror();
return;
}
clearUserMirror();
window.location.replace(loginUrl());
}
window.CrankAuth = {
fetchSession: fetchSession,
replaceSession: replaceSession,
getCachedSession: function() { return sessionCache; },
renderShellIdentity: function() {
renderShellIdentity(sessionCache);
},
guardProtectedPage: guardProtectedPage,
guardLoginPage: guardLoginPage,
login: login,
logout: logout,
handleUnauthorized: handleUnauthorized,
};
window.addEventListener('crank:workspacechange', function() {
renderShellIdentity(sessionCache);
});
window.addEventListener('crank:sessionchange', function(event) {
renderShellIdentity(event.detail || sessionCache);
});
document.addEventListener('DOMContentLoaded', function() {
renderShellIdentity(sessionCache);
});
}());
+535
View File
@@ -0,0 +1,535 @@
const PAGE_SIZE_DESKTOP = 6;
const PAGE_SIZE_MOBILE = 4;
const MOBILE_BREAKPOINT = 960;
function getSortOptions() {
return [
{ value: 'created_desc', label: (window.t && t('sort.created_desc')) || 'Sort: Last created' },
{ value: 'created_asc', label: (window.t && t('sort.created_asc')) || 'Sort: Oldest first' },
{ value: 'name_asc', label: (window.t && t('sort.name_asc')) || 'Sort: Name AZ' },
{ value: 'name_desc', label: (window.t && t('sort.name_desc')) || 'Sort: Name ZA' },
];
}
function getTabDefs() {
return [
{ id: 'all', label: (window.t && t('tab.all')) || 'All' },
{ id: 'active', label: (window.t && t('tab.active')) || 'Active' },
{ id: 'draft', label: (window.t && t('tab.draft')) || 'Draft' },
{ id: 'error', label: (window.t && t('tab.error')) || 'Error' },
{ id: 'inactive', label: (window.t && t('tab.inactive')) || 'Inactive' },
];
}
function uiStatus(rawStatus) {
if (rawStatus === 'published') return 'active';
if (rawStatus === 'archived') return 'inactive';
if (rawStatus === 'testing') return 'active';
return rawStatus || 'draft';
}
function currentLocale() {
return localStorage.getItem('crank_lang') === 'ru' ? 'ru-RU' : 'en-US';
}
function mapOperation(item) {
var mapped = {
id: item.id,
name: item.name,
display_name: item.display_name,
protocol: item.protocol,
status: uiStatus(item.status),
raw_status: item.status,
category: item.category || 'general',
created_at: item.created_at,
updated_at: item.updated_at,
published_at: item.published_at,
target_url: item.target_url || '',
method: item.target_action || '',
usage_summary: item.usage_summary || {
calls_today: 0,
error_rate_pct: 0,
avg_latency_ms: 0,
},
agent_refs: item.agent_refs || [],
};
return window.localizeDemoOperation ? window.localizeDemoOperation(mapped) : mapped;
}
function emptyStats() {
return {
total: 0,
active: 0,
requestsToday: 0,
averageLatencyMs: 0,
};
}
function computeStats(operations) {
if (!operations.length) return emptyStats();
var requestsToday = operations.reduce(function(sum, operation) {
return sum + (operation.usage_summary ? operation.usage_summary.calls_today : 0);
}, 0);
var latencyBase = operations.filter(function(operation) {
return operation.usage_summary && operation.usage_summary.calls_today > 0;
});
var latencyWeight = latencyBase.reduce(function(sum, operation) {
return sum + operation.usage_summary.calls_today;
}, 0);
var weightedLatency = latencyBase.reduce(function(sum, operation) {
return sum + (operation.usage_summary.avg_latency_ms * operation.usage_summary.calls_today);
}, 0);
return {
total: operations.length,
active: operations.filter(function(operation) { return operation.status === 'active'; }).length,
requestsToday: requestsToday,
averageLatencyMs: latencyWeight > 0 ? Math.round(weightedLatency / latencyWeight) : 0,
};
}
document.addEventListener('alpine:init', function() {
Alpine.data('catalog', function() {
return {
operations: [],
loading: true,
loadError: '',
pageSize: PAGE_SIZE_DESKTOP,
search: '',
tab: 'all',
filterProtocol: null,
filterCategory: null,
filterAgent: null,
sort: 'created_desc',
page: 1,
openDropdown: null,
navOpen: false,
categoryOptions: [],
workspaceId: null,
stats: emptyStats(),
_opsVersion: 0,
_nonStatusViewCache: null,
_nonStatusViewKey: '',
_filteredCache: null,
_filteredKey: '',
_agentsByOpIdCache: null,
_agentsByOpIdCacheVersion: -1,
_activeAgentsCache: null,
_activeAgentsCacheVersion: -1,
async init() {
var self = this;
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
this.workspaceId = workspace ? workspace.id : null;
var updatePageSize = function() {
self.pageSize = window.innerWidth <= MOBILE_BREAKPOINT ? PAGE_SIZE_MOBILE : PAGE_SIZE_DESKTOP;
self.page = 1;
};
updatePageSize();
window.addEventListener('resize', updatePageSize);
window.addEventListener('crank:langchange', function() {
self.sort = self.sort;
applyLang();
});
window.addEventListener('crank:workspacechange', async function(event) {
self.workspaceId = event.detail ? event.detail.id : null;
await self.reload();
});
document.addEventListener('click', function(event) {
if (!event.target.closest('.filter-dropdown, .sort-dropdown, .user-menu, .ws-switcher')) {
self.openDropdown = null;
}
if (!event.target.closest('.navbar')) {
self.navOpen = false;
}
});
await this.reload();
},
async reload() {
this.loading = true;
this.loadError = '';
try {
var response = await window.CrankApi.listOperations(this.workspaceId);
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) {
this.replaceOperations([]);
this.categoryOptions = [];
this.stats = emptyStats();
this.loadError = error.message || 'Failed to load operations';
}
this.loading = false;
this.page = 1;
},
replaceOperations(operations) {
this.operations = operations;
this._opsVersion += 1;
this._nonStatusViewCache = null;
this._nonStatusViewKey = '';
this._filteredCache = null;
this._filteredKey = '';
this._agentsByOpIdCache = null;
this._agentsByOpIdCacheVersion = -1;
this._activeAgentsCache = null;
this._activeAgentsCacheVersion = -1;
},
_applyNonStatusFilters(operations) {
var filtered = operations;
if (this.search.trim()) {
var query = this.search.toLowerCase();
filtered = filtered.filter(function(operation) {
return operation.name.toLowerCase().includes(query)
|| operation.display_name.toLowerCase().includes(query)
|| operation.category.toLowerCase().includes(query)
|| (operation.target_url || '').toLowerCase().includes(query);
});
}
if (this.filterProtocol) {
filtered = filtered.filter(function(operation) { return operation.protocol === this.filterProtocol; }, this);
}
if (this.filterCategory) {
filtered = filtered.filter(function(operation) { return operation.category === this.filterCategory; }, this);
}
if (this.filterAgent) {
filtered = filtered.filter(function(operation) {
return (operation.agent_refs || []).some(function(agent) { return agent.agent_id === this.filterAgent; }, this);
}, this);
}
return filtered;
},
_nonStatusView() {
var key = [
this._opsVersion,
this.search.trim(),
this.filterProtocol || '',
this.filterCategory || '',
this.filterAgent || '',
].join('|');
if (this._nonStatusViewCache && this._nonStatusViewKey === key) {
return this._nonStatusViewCache;
}
var operations = this._applyNonStatusFilters(this.operations);
var tabCounts = {
all: operations.length,
active: 0,
draft: 0,
error: 0,
inactive: 0,
};
operations.forEach(function(operation) {
if (Object.prototype.hasOwnProperty.call(tabCounts, operation.status)) {
tabCounts[operation.status] += 1;
}
});
this._nonStatusViewKey = key;
this._nonStatusViewCache = {
operations: operations,
tabCounts: tabCounts,
};
return this._nonStatusViewCache;
},
get filtered() {
var nonStatusView = this._nonStatusView();
var key = [
this._nonStatusViewKey,
this.tab,
this.sort,
].join('|');
if (this._filteredCache && this._filteredKey === key) {
return this._filteredCache;
}
var operations = nonStatusView.operations;
if (this.tab !== 'all') {
operations = operations.filter(function(operation) {
return operation.status === this.tab;
}, this);
}
operations = operations.slice().sort(function(left, right) {
if (this.sort === 'created_desc') return new Date(right.created_at) - new Date(left.created_at);
if (this.sort === 'created_asc') return new Date(left.created_at) - new Date(right.created_at);
if (this.sort === 'name_asc') return left.name.localeCompare(right.name);
if (this.sort === 'name_desc') return right.name.localeCompare(left.name);
return 0;
}.bind(this));
this._filteredKey = key;
this._filteredCache = operations;
return this._filteredCache;
},
get totalFiltered() {
return this.filtered.length;
},
get totalPages() {
return Math.max(1, Math.ceil(this.totalFiltered / this.pageSize));
},
get paginated() {
var start = (this.page - 1) * this.pageSize;
return this.filtered.slice(start, start + this.pageSize);
},
get pageStart() {
return this.totalFiltered === 0 ? 0 : (this.page - 1) * this.pageSize + 1;
},
get pageEnd() {
return Math.min(this.page * this.pageSize, this.totalFiltered);
},
tabCount(tab) {
var tabCounts = this._nonStatusView().tabCounts;
return Object.prototype.hasOwnProperty.call(tabCounts, tab) ? tabCounts[tab] : 0;
},
get activeChips() {
var chips = [];
if (this.filterProtocol) chips.push({ key: 'protocol', label: this.tfKey('ops.filter.protocol', { value: this.filterProtocol.toUpperCase() }) });
if (this.filterCategory) chips.push({ key: 'category', label: this.tfKey('ops.filter.category', { value: this.filterCategory }) });
if (this.filterAgent) {
var agent = this.activeAgents.find(function(item) { return item.agent_id === this.filterAgent; }, this);
chips.push({ key: 'agent', label: this.tfKey('ops.filter.agent', { value: agent ? agent.display_name : '' }) });
}
return chips;
},
get hasActiveFilters() {
return !!(this.filterProtocol || this.filterCategory || this.filterAgent || this.search.trim());
},
clearChip(key) {
if (key === 'protocol') this.filterProtocol = null;
if (key === 'category') this.filterCategory = null;
if (key === 'agent') this.filterAgent = null;
this.page = 1;
},
clearAllFilters() {
this.filterProtocol = null;
this.filterCategory = null;
this.filterAgent = null;
this.search = '';
this.tab = 'all';
this.page = 1;
},
get agentsByOpId() {
if (this._agentsByOpIdCache && this._agentsByOpIdCacheVersion === this._opsVersion) {
return this._agentsByOpIdCache;
}
var map = {};
this.operations.forEach(function(operation) {
map[operation.id] = operation.agent_refs || [];
});
this._agentsByOpIdCacheVersion = this._opsVersion;
this._agentsByOpIdCache = map;
return this._agentsByOpIdCache;
},
get activeAgents() {
if (this._activeAgentsCache && this._activeAgentsCacheVersion === this._opsVersion) {
return this._activeAgentsCache;
}
var seen = {};
var agents = [];
this.operations.forEach(function(operation) {
(operation.agent_refs || []).forEach(function(agent) {
if (!seen[agent.agent_id]) {
seen[agent.agent_id] = true;
agents.push(agent);
}
});
});
this._activeAgentsCacheVersion = this._opsVersion;
this._activeAgentsCache = agents.sort(function(left, right) {
return left.display_name.localeCompare(right.display_name);
});
return this._activeAgentsCache;
},
get sortLabel() {
return getSortOptions().find(function(option) { return option.value === this.sort; }, this)?.label || 'Sort';
},
setTab(tab) {
this.tab = tab;
this.page = 1;
},
setSearch(value) {
this.search = value;
this.page = 1;
},
setProtocol(value) {
this.filterProtocol = this.filterProtocol === value ? null : value;
this.openDropdown = null;
this.page = 1;
},
setCategory(value) {
this.filterCategory = this.filterCategory === value ? null : value;
this.openDropdown = null;
this.page = 1;
},
setSort(value) {
this.sort = value;
this.openDropdown = null;
},
setAgent(value) {
this.filterAgent = this.filterAgent === value ? null : value;
this.openDropdown = null;
this.page = 1;
},
clearProtocol() {
this.filterProtocol = null;
this.openDropdown = null;
this.page = 1;
},
clearCategory() {
this.filterCategory = null;
this.openDropdown = null;
this.page = 1;
},
clearAgent() {
this.filterAgent = null;
this.openDropdown = null;
this.page = 1;
},
toggleDropdown(name) {
this.openDropdown = this.openDropdown === name ? null : name;
},
goPage(page) {
if (page >= 1 && page <= this.totalPages) this.page = page;
},
editOperation(operation) {
window.location.href = ((window.CrankRoutes && window.CrankRoutes.wizard) || '/wizard/')
+ '?mode=edit&operationId=' + encodeURIComponent(operation.id);
},
async deleteOperation(id) {
if (!confirm(this.tKey('ops.delete.confirm'))) return;
try {
var operation = this.operations.find(function(item) { return item.id === id; });
await window.CrankApi.deleteOperation(this.workspaceId, id);
this.replaceOperations(this.operations.filter(function(operation) { return operation.id !== id; }));
this.categoryOptions = Array.from(new Set(this.operations.map(function(operation) {
return operation.category;
}).filter(Boolean))).sort();
this.stats = computeStats(this.operations);
if (window.CrankUi) {
window.CrankUi.success(
this.tfKey('ops.delete.success.message', {
name: operation ? (operation.display_name || operation.name) : ''
}),
this.tKey('ops.delete.success.title')
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || this.tKey('ops.delete.error.message'),
this.tKey('ops.delete.error.title')
);
}
}
},
protocolLabel(operation) {
return operation.method ? ('REST · ' + operation.method) : 'REST';
},
protocolClass(operation) {
return 'badge badge-' + operation.protocol;
},
statusClass(operation) {
return 'status-badge status-' + operation.status;
},
statusLabel(operation) {
if (operation.status === 'active') return this.tKey('tab.active');
if (operation.status === 'inactive') return this.tKey('tab.inactive');
if (operation.status === 'error') return this.tKey('tab.error');
return this.tKey('tab.draft');
},
formatDate(dateStr) {
if (!dateStr) return '—';
return new Date(dateStr).toLocaleDateString(currentLocale(), { month: 'short', day: 'numeric', year: 'numeric' });
},
truncateUrl(url) {
if (!url) return '—';
return url.length > 42 ? url.slice(0, 42) + '…' : url;
},
formatMetricNumber(value) {
return new Intl.NumberFormat(currentLocale()).format(value || 0);
},
tKey(key) {
return window.t ? t(key) : key;
},
tfKey(key, vars) {
return window.tf ? tf(key, vars) : this.tKey(key);
},
get sortOptions() {
return getSortOptions();
},
get tabDefs() {
return getTabDefs();
},
handleNewOperation() {
window.location.href = (window.CrankRoutes && window.CrankRoutes.wizard) || '/wizard/';
},
handleLogout() {
window.CrankAuth.logout();
},
};
});
});
+16
View File
@@ -0,0 +1,16 @@
(function() {
window.APP_BASE = '/';
window.DATA_URL = '/data/';
window.CrankRoutes = {
home: '/',
login: '/login',
agents: '/agents',
apiKeys: '/api-keys',
secrets: '/secrets',
logs: '/logs',
usage: '/usage',
settings: '/settings',
workspaceSetup: '/workspace-setup',
wizard: '/wizard/'
};
}());
+111
View File
@@ -0,0 +1,111 @@
(function() {
var currentPage = '';
function messageFor(error) {
if (!error) {
return 'unknown error';
}
if (error.message) {
return error.message;
}
return String(error);
}
function setPage(pageName) {
currentPage = pageName || '';
document.documentElement.setAttribute('data-crank-page', currentPage || 'unknown');
}
function markBootstrapState(pageName, status, details) {
document.documentElement.setAttribute('data-crank-bootstrap-page', pageName || currentPage || 'unknown');
document.documentElement.setAttribute('data-crank-bootstrap-state', status);
if (status === 'error' && details) {
document.documentElement.setAttribute('data-crank-bootstrap-error', details);
return;
}
document.documentElement.removeAttribute('data-crank-bootstrap-error');
}
function report(stage, error, pageName) {
var resolvedPage = pageName || currentPage || 'unknown';
console.error('[CrankUI][' + resolvedPage + '][' + stage + ']', error);
window.dispatchEvent(new CustomEvent('crank:ui-error', {
detail: {
page: resolvedPage,
stage: stage,
message: messageFor(error),
},
}));
}
function bootstrap(pageName, init) {
function run() {
setPage(pageName);
markBootstrapState(pageName, 'pending');
try {
var result = init();
if (result && typeof result.then === 'function') {
result.then(function() {
markBootstrapState(pageName, 'ready');
}).catch(function(error) {
markBootstrapState(pageName, 'error', messageFor(error));
report('bootstrap', error, pageName);
});
return;
}
markBootstrapState(pageName, 'ready');
} catch (error) {
markBootstrapState(pageName, 'error', messageFor(error));
report('bootstrap', error, pageName);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run, { once: true });
return;
}
run();
}
function ensureTestId(element, value) {
if (!element || !value) {
return;
}
element.setAttribute('data-testid', value);
}
function ensureShellTestIds() {
document.querySelectorAll('.nav-avatar').forEach(function(element) {
ensureTestId(element, 'shell-avatar');
});
document.querySelectorAll('.user-dropdown-name').forEach(function(element) {
ensureTestId(element, 'shell-user-name');
});
document.querySelectorAll('.user-dropdown-role').forEach(function(element) {
ensureTestId(element, 'shell-user-role');
});
ensureTestId(document.getElementById('ws-current-name'), 'shell-workspace-name');
}
window.addEventListener('error', function(event) {
if (!currentPage) {
return;
}
report('runtime', event.error || new Error(event.message || 'window error'));
});
window.addEventListener('unhandledrejection', function(event) {
if (!currentPage) {
return;
}
report('unhandledrejection', event.reason || new Error('unhandled rejection'));
});
window.CrankDiagnostics = {
bootstrap: bootstrap,
ensureTestId: ensureTestId,
ensureShellTestIds: ensureShellTestIds,
report: report,
setPage: setPage,
};
}());
+32
View File
@@ -0,0 +1,32 @@
(function() {
function clear(element) {
if (!element) {
return;
}
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
function createEmptyState(title, body) {
var root = document.createElement('div');
root.className = 'empty-state';
var titleNode = document.createElement('div');
titleNode.className = 'empty-state-title';
titleNode.textContent = title;
root.appendChild(titleNode);
var bodyNode = document.createElement('div');
bodyNode.className = 'empty-state-text';
bodyNode.textContent = body;
root.appendChild(bodyNode);
return root;
}
window.CrankDom = {
clear: clear,
createEmptyState: createEmptyState,
};
}());
+2081
View File
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
(function() {
function tKey(key) {
return window.t ? t(key) : key;
}
function showError(message) {
var errorElement = document.getElementById('login-error');
errorElement.classList.add('is-visible');
errorElement.textContent = message;
}
function hideError() {
var errorElement = document.getElementById('login-error');
errorElement.classList.remove('is-visible');
}
function initLoginPage() {
window.CrankAuth.guardLoginPage().catch(function(error) {
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.report === 'function') {
window.CrankDiagnostics.report('guard-login-page', error, 'login');
}
});
document.getElementById('login-form').addEventListener('submit', async function(event) {
event.preventDefault();
var email = document.getElementById('email').value.trim();
var password = document.getElementById('password').value;
if (!email || !password) {
showError(tKey('login.error.required'));
return;
}
hideError();
try {
await window.CrankAuth.login(email, password);
} catch (error) {
if (error && error.status === 401) {
showError(tKey('login.error.invalid'));
return;
}
showError(tKey('login.error.generic'));
}
});
}
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') {
window.CrankDiagnostics.bootstrap('login', initLoginPage);
return;
}
document.addEventListener('DOMContentLoaded', initLoginPage);
}());
+419
View File
@@ -0,0 +1,419 @@
document.addEventListener('DOMContentLoaded', function () {
var state = {
logs: [],
details: {},
level: 'all',
search: '',
period: '7d',
openId: null,
liveMode: true,
timer: null,
workspaceId: null,
loading: false,
loadError: '',
};
var logList = document.getElementById('log-list');
var logSearch = document.getElementById('log-search');
var refreshBtn = document.getElementById('refresh-btn');
var timeRangeSel = document.getElementById('time-range');
var liveDot = document.querySelector('.live-dot');
var liveLabel = document.querySelector('.live-label');
function tKey(key) {
return window.t ? t(key) : key;
}
function currentWorkspaceId() {
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
return workspace ? workspace.id : null;
}
function durationLabel(durationMs) {
if (!durationMs) {
return '0ms';
}
if (durationMs >= 1000) {
return (durationMs / 1000).toFixed(1) + 's';
}
return durationMs + 'ms';
}
function formatJson(value) {
if (value === null || value === undefined) {
return '';
}
if (typeof value === 'string') {
return value;
}
return JSON.stringify(value, null, 2);
}
function formatTime(timestamp) {
var date = new Date(timestamp);
return date.toISOString().slice(11, 23);
}
function element(tag, className, text) {
var node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined && text !== null) node.textContent = text;
return node;
}
function statusClass(statusCode) {
if (!statusCode) {
return '';
}
if (statusCode < 300) {
return 'ok';
}
if (statusCode < 500) {
return 'warn';
}
return 'err';
}
function normalizeLog(record) {
var log = record.log;
var agent = window.localizeDemoAgent
? window.localizeDemoAgent({
slug: record.agent_slug,
display_name: record.agent_display_name,
description: '',
})
: null;
var operation = window.localizeDemoOperation
? window.localizeDemoOperation({
name: record.operation_name,
operation_display_name: record.operation_display_name,
})
: null;
return {
id: log.id,
createdAt: log.created_at,
level: log.level,
source: log.source,
status: log.status,
statusCode: log.status_code,
durationMs: log.duration_ms,
toolName: log.tool_name,
message: log.message,
operationName: record.operation_name,
operationDisplayName: operation ? operation.operation_display_name : record.operation_display_name,
agentSlug: record.agent_slug,
agentDisplayName: agent ? agent.display_name : record.agent_display_name,
requestPreview: log.request_preview,
responsePreview: log.response_preview,
errorKind: log.error_kind,
requestId: log.request_id,
};
}
function renderEmpty(title, message) {
logList.innerHTML = '';
var empty = element('div', 'empty-state');
empty.appendChild(element('div', 'empty-state-title', title));
empty.appendChild(element('div', 'empty-state-text', message));
logList.appendChild(empty);
}
function renderLogs() {
if (state.loading && state.logs.length === 0) {
renderEmpty(tKey('logs.loading.title'), tKey('logs.loading.sub'));
return;
}
if (state.loadError) {
renderEmpty(tKey('logs.error.title'), state.loadError);
return;
}
if (!state.logs.length) {
renderEmpty(
tKey('logs.empty.title'),
state.search || state.level !== 'all'
? tKey('logs.empty.filtered')
: tKey('logs.empty.initial')
);
return;
}
var fragment = document.createDocumentFragment();
state.logs.forEach(function (item) {
var row = document.createElement('div');
row.className = 'log-entry';
row.setAttribute('data-id', item.id);
var timeEl = document.createElement('span');
timeEl.className = 'log-time';
timeEl.textContent = formatTime(item.createdAt);
row.appendChild(timeEl);
var levelEl = document.createElement('span');
levelEl.className = 'log-level ' + item.level;
levelEl.textContent = item.level.toUpperCase();
row.appendChild(levelEl);
var msgDiv = document.createElement('div');
msgDiv.className = 'log-msg';
var opSpan = document.createElement('span');
opSpan.className = 'log-op-name';
opSpan.textContent = item.operationDisplayName || item.operationName;
msgDiv.appendChild(opSpan);
msgDiv.appendChild(document.createTextNode('\u00a0\u00a0'));
if (item.statusCode) {
var statusEl = document.createElement('span');
statusEl.className = 'log-status ' + statusClass(item.statusCode);
statusEl.textContent = item.statusCode;
msgDiv.appendChild(statusEl);
msgDiv.appendChild(document.createTextNode(' \u00b7 '));
}
msgDiv.appendChild(document.createTextNode(item.message));
row.appendChild(msgDiv);
var durationEl = document.createElement('span');
var durationClass = item.level === 'error' ? ' error' : (item.durationMs >= 1000 ? ' slow' : '');
durationEl.className = 'log-duration' + durationClass;
durationEl.textContent = durationLabel(item.durationMs);
row.appendChild(durationEl);
fragment.appendChild(row);
var expanded = document.createElement('div');
expanded.className = 'log-entry-expanded' + (item.id === state.openId ? ' open' : '');
expanded.setAttribute('data-exp', item.id);
if (item.id === state.openId) {
var detail = state.details[item.id] || item;
if (detail.agentDisplayName || detail.agentSlug) {
var agentLabel = document.createElement('div');
agentLabel.className = 'log-detail-label';
agentLabel.textContent = tKey('logs.detail.agent');
expanded.appendChild(agentLabel);
var agentPre = document.createElement('pre');
agentPre.className = 'log-detail-block';
agentPre.textContent = detail.agentDisplayName || detail.agentSlug;
expanded.appendChild(agentPre);
}
var requestLabel = document.createElement('div');
requestLabel.className = 'log-detail-label';
requestLabel.textContent = tKey('logs.detail.request');
expanded.appendChild(requestLabel);
var requestPre = document.createElement('pre');
requestPre.className = 'log-detail-block';
requestPre.textContent = formatJson(detail.requestPreview);
expanded.appendChild(requestPre);
var responseLabel = document.createElement('div');
responseLabel.className = 'log-detail-label';
responseLabel.textContent = tKey('logs.detail.response');
expanded.appendChild(responseLabel);
var responsePre = document.createElement('pre');
responsePre.className = 'log-detail-block';
responsePre.textContent = formatJson(detail.responsePreview);
expanded.appendChild(responsePre);
if (detail.errorKind || detail.requestId) {
var metaLabel = document.createElement('div');
metaLabel.className = 'log-detail-label';
metaLabel.textContent = tKey('logs.detail.meta');
expanded.appendChild(metaLabel);
var metaPre = document.createElement('pre');
metaPre.className = 'log-detail-block';
metaPre.textContent = formatJson({
request_id: detail.requestId || null,
error_kind: detail.errorKind || null,
source: detail.source,
status: detail.status,
});
expanded.appendChild(metaPre);
}
}
fragment.appendChild(expanded);
});
logList.innerHTML = '';
logList.appendChild(fragment);
logList.querySelectorAll('.log-entry').forEach(function (row) {
row.addEventListener('click', async function () {
var id = this.getAttribute('data-id');
state.openId = state.openId === id ? null : id;
renderLogs();
if (state.openId) {
await loadLogDetail(id);
}
});
});
}
function queryParams() {
var params = {
period: state.period,
limit: 100,
};
if (state.level !== 'all') {
params.level = state.level;
}
if (state.search) {
params.search = state.search;
}
return params;
}
async function loadLogs() {
if (!window.CrankApi) {
state.loadError = tKey('logs.error.api');
renderLogs();
return;
}
state.workspaceId = currentWorkspaceId();
if (!state.workspaceId) {
state.loadError = tKey('logs.error.workspace');
renderLogs();
return;
}
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);
if (state.openId && !state.logs.some(function (item) { return item.id === state.openId; })) {
state.openId = null;
}
} catch (error) {
state.loadError = error.message || tKey('logs.error.load');
} finally {
state.loading = false;
renderLogs();
}
}
async function loadLogDetail(logId) {
if (!window.CrankApi || !state.workspaceId || state.details[logId]) {
return;
}
try {
var record = await window.CrankApi.getLog(state.workspaceId, logId);
state.details[logId] = normalizeLog(record);
if (state.openId === logId) {
renderLogs();
}
} catch (_error) {
}
}
function setLiveState() {
if (liveDot) {
liveDot.classList.toggle('is-paused', !state.liveMode);
}
if (liveLabel) {
liveLabel.textContent = state.liveMode ? tKey('logs.live') : tKey('logs.paused');
liveLabel.classList.toggle('is-paused', !state.liveMode);
}
}
function stopPolling() {
if (state.timer) {
clearInterval(state.timer);
state.timer = null;
}
}
function startPolling() {
stopPolling();
if (!state.liveMode) {
return;
}
state.timer = setInterval(loadLogs, 4000);
}
function toggleLive() {
state.liveMode = !state.liveMode;
setLiveState();
startPolling();
if (window.CrankUi) {
window.CrankUi.info(
state.liveMode ? tKey('logs.live.on.body') : tKey('logs.live.off.body'),
state.liveMode ? tKey('logs.live.on.title') : tKey('logs.live.off.title')
);
}
}
document.querySelectorAll('.filter-chip[data-level]').forEach(function (button) {
button.addEventListener('click', function () {
state.level = this.getAttribute('data-level');
document.querySelectorAll('.filter-chip[data-level]').forEach(function (item) {
item.classList.remove('active');
});
this.classList.add('active');
loadLogs();
});
});
if (logSearch) {
logSearch.addEventListener('input', function () {
state.search = this.value.trim();
loadLogs();
});
}
if (refreshBtn) {
refreshBtn.addEventListener('click', function () {
loadLogs().then(function () {
if (!state.loadError && window.CrankUi) {
window.CrankUi.info(tKey('logs.refresh.body'), tKey('logs.refresh.title'));
}
});
});
}
if (timeRangeSel) {
timeRangeSel.value = state.period;
timeRangeSel.addEventListener('change', function () {
state.period = this.value;
loadLogs();
});
}
if (liveDot) {
liveDot.addEventListener('click', toggleLive);
}
if (liveLabel) {
liveLabel.addEventListener('click', toggleLive);
}
window.addEventListener('crank:workspacechange', function () {
state.details = {};
state.openId = null;
loadLogs();
});
setLiveState();
startPolling();
if (window.whenWorkspacesReady) {
window.whenWorkspacesReady().finally(loadLogs);
} else {
loadLogs();
}
});
+76
View File
@@ -0,0 +1,76 @@
(function () {
function initNav() {
if (window.CrankAuth && typeof window.CrankAuth.renderShellIdentity === 'function') {
window.CrankAuth.renderShellIdentity();
}
var dropdown = document.querySelector('.user-dropdown');
var avatar = document.querySelector('.nav-avatar');
var hamburger = document.querySelector('.nav-hamburger');
var mobileNav = document.querySelector('.mobile-nav');
if (dropdown) {
dropdown.hidden = true;
}
if (mobileNav) {
mobileNav.hidden = true;
}
if (avatar && dropdown) {
avatar.addEventListener('click', function (event) {
event.stopPropagation();
dropdown.hidden = !dropdown.hidden;
});
}
if (hamburger && mobileNav) {
hamburger.addEventListener('click', function (event) {
event.stopPropagation();
mobileNav.hidden = !mobileNav.hidden;
hamburger.classList.toggle('open', !mobileNav.hidden);
});
}
document.addEventListener('click', function (event) {
if (dropdown && !event.target.closest('.user-menu')) {
dropdown.hidden = true;
}
if (mobileNav && !event.target.closest('.navbar') && !event.target.closest('.mobile-nav')) {
mobileNav.hidden = true;
if (hamburger) {
hamburger.classList.remove('open');
}
}
});
document.querySelectorAll('[data-action="logout"]').forEach(function (button) {
button.addEventListener('click', function () {
window.CrankAuth.logout();
});
});
document.querySelectorAll('[data-action="profile"]').forEach(function (button) {
button.addEventListener('click', function () {
if (dropdown) {
dropdown.hidden = true;
}
window.location.href = ((window.CrankRoutes && window.CrankRoutes.settings) || '/settings') + '#profile';
});
});
document.querySelectorAll('[data-action="settings-link"]').forEach(function (button) {
button.addEventListener('click', function () {
if (dropdown) {
dropdown.hidden = true;
}
window.location.href = (window.CrankRoutes && window.CrankRoutes.settings) || '/settings';
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initNav);
} else {
initNav();
}
})();
+75
View File
@@ -0,0 +1,75 @@
(function() {
var manifestPromise = null;
var loadedScripts = Object.create(null);
function overlayBasePath() {
return (window.APP_BASE || '/') + 'overlay/';
}
function fetchManifest() {
return fetch(overlayBasePath() + 'manifest.json', { cache: 'no-store' })
.then(function(response) {
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error('overlay manifest request failed');
}
return response.json();
})
.catch(function() {
return null;
});
}
function loadScript(path) {
if (loadedScripts[path]) {
return loadedScripts[path];
}
loadedScripts[path] = new Promise(function(resolve, reject) {
var script = document.createElement('script');
script.src = overlayBasePath() + path.replace(/^\/+/, '');
script.async = false;
script.onload = function() { resolve(); };
script.onerror = function() { reject(new Error('overlay script load failed')); };
document.head.appendChild(script);
}).catch(function() {
return null;
});
return loadedScripts[path];
}
function load() {
if (manifestPromise) {
return manifestPromise;
}
manifestPromise = fetchManifest().then(function(manifest) {
if (!(manifest && manifest.slots && window.CrankUiSlots)) {
return null;
}
var tasks = Object.entries(manifest.slots)
.filter(function(entry) {
return window.CrankUiSlots.isAllowed(entry[0]) && typeof entry[1] === 'string';
})
.map(function(entry) {
return loadScript(entry[1]);
});
return Promise.all(tasks).then(function() {
return manifest;
});
});
return manifestPromise;
}
async function render(root, context) {
await load();
if (window.CrankUiSlots) {
window.CrankUiSlots.renderAll(root, context);
}
}
window.CrankOverlay = {
load: load,
render: render,
};
}());
+709
View File
@@ -0,0 +1,709 @@
function initSecretsPage() {
var state = {
workspaceId: null,
secrets: [],
profiles: [],
derived: {
activeSecretCount: 0,
secretsById: {},
usageBySecret: {},
},
search: '',
loading: false,
error: '',
modalMode: 'create',
modalSecretId: null,
};
var modal = document.getElementById('secret-modal');
var tbody = document.getElementById('secrets-tbody');
var cardList = document.getElementById('secrets-card-list');
var profilesList = document.getElementById('auth-profiles-list');
var summary = document.getElementById('secrets-summary');
var profilesSummary = document.getElementById('secret-profiles-summary');
var searchInput = document.getElementById('secret-search');
var modalTitle = document.getElementById('secret-modal-title');
var modalName = document.getElementById('secret-name');
var modalKind = document.getElementById('secret-kind');
var modalNote = document.getElementById('secret-modal-note');
var modalSubmit = document.getElementById('secret-modal-submit-btn');
var kindHint = document.getElementById('secret-kind-hint');
var stringLabel = document.getElementById('secret-string-label');
var stringField = document.getElementById('secret-string-value');
var usernameField = document.getElementById('secret-username');
var passwordField = document.getElementById('secret-password');
var jsonField = document.getElementById('secret-json-value');
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : tKey(key);
}
function currentWorkspaceId() {
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
return workspace ? workspace.id : null;
}
function currentWorkspace() {
return window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
}
function secretKindLabel(kind) {
return tKey('secrets.kind.' + String(kind || '').toLowerCase());
}
function authKindLabel(kind) {
return tKey('secrets.auth_kind.' + String(kind || '').toLowerCase());
}
function formatDate(value) {
if (!value) return '—';
return new Date(value).toLocaleDateString();
}
function secretIdsForProfile(profile) {
var config = profile && profile.config ? profile.config : {};
if (config.bearer) return [config.bearer.secret_id];
if (config.basic) return [config.basic.username_secret_id, config.basic.password_secret_id];
if (config.api_key_header) return [config.api_key_header.secret_id];
if (config.api_key_query) return [config.api_key_query.secret_id];
return [];
}
function profileSummary(profile, secretsById) {
var config = profile && profile.config ? profile.config : {};
if (config.bearer) {
return tfKey('secrets.profiles.summary_bearer', {
header: config.bearer.header_name,
secret: secretName(secretsById, config.bearer.secret_id),
});
}
if (config.basic) {
return tfKey('secrets.profiles.summary_basic', {
username: secretName(secretsById, config.basic.username_secret_id),
password: secretName(secretsById, config.basic.password_secret_id),
});
}
if (config.api_key_header) {
return tfKey('secrets.profiles.summary_api_key_header', {
header: config.api_key_header.header_name,
secret: secretName(secretsById, config.api_key_header.secret_id),
});
}
if (config.api_key_query) {
return tfKey('secrets.profiles.summary_api_key_query', {
param: config.api_key_query.param_name,
secret: secretName(secretsById, config.api_key_query.secret_id),
});
}
return tKey('secrets.profiles.summary_unknown');
}
function secretName(secretsById, secretId) {
var secret = secretsById[secretId];
return secret ? secret.name : secretId;
}
function buildUsageBySecret() {
var usage = {};
state.profiles.forEach(function (profile) {
secretIdsForProfile(profile).forEach(function (secretId) {
if (!secretId) return;
if (!usage[secretId]) usage[secretId] = [];
usage[secretId].push(profile);
});
});
return usage;
}
function recomputeDerivedState() {
var secretsById = {};
var activeSecretCount = 0;
state.secrets.forEach(function (secret) {
secretsById[secret.id] = secret;
if (secret.status === 'active') {
activeSecretCount += 1;
}
});
state.derived = {
activeSecretCount: activeSecretCount,
secretsById: secretsById,
usageBySecret: buildUsageBySecret(),
};
}
function resetModalFields() {
modalName.value = '';
modalKind.value = 'token';
stringField.value = '';
usernameField.value = '';
passwordField.value = '';
jsonField.value = '{\n "value": ""\n}';
}
function openModal(mode, secret) {
state.modalMode = mode;
state.modalSecretId = secret ? secret.id : null;
resetModalFields();
if (mode === 'rotate' && secret) {
modalTitle.textContent = tKey('secrets.modal.rotate_title');
modalName.value = secret.name;
modalName.disabled = true;
modalKind.value = secret.kind;
modalKind.disabled = true;
modalSubmit.textContent = tKey('secrets.modal.rotate_action');
modalNote.textContent = tKey('secrets.modal.rotate_note');
} else {
modalTitle.textContent = tKey('secrets.modal.create_title');
modalName.disabled = false;
modalKind.disabled = false;
modalSubmit.textContent = tKey('secrets.modal.create_action');
modalNote.textContent = tKey('secrets.modal.create_note');
}
updateKindFields();
modal.classList.add('open');
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.ensureTestId === 'function') {
window.CrankDiagnostics.ensureTestId(modal, mode === 'rotate' ? 'secret-rotate-modal' : 'secret-create-modal');
}
setTimeout(function () {
if (mode === 'rotate') {
stringField.focus();
} else {
modalName.focus();
}
}, 30);
}
function closeModal() {
modal.classList.remove('open');
state.modalMode = 'create';
state.modalSecretId = null;
}
function updateKindFields() {
var kind = modalKind.value;
document.querySelectorAll('[data-kind-field="string"]').forEach(function (element) {
element.hidden = !(kind === 'token' || kind === 'header');
});
document.querySelectorAll('[data-kind-field="basic"]').forEach(function (element) {
element.hidden = kind !== 'username_password';
});
document.querySelectorAll('[data-kind-field="json"]').forEach(function (element) {
element.hidden = kind !== 'generic';
});
if (kind === 'token') {
stringLabel.textContent = tKey('secrets.modal.value');
} else if (kind === 'header') {
stringLabel.textContent = tKey('secrets.modal.header_value');
} else {
stringLabel.textContent = tKey('secrets.modal.value');
}
kindHint.textContent = tKey('secrets.hint.' + kind);
}
function buildSecretValue() {
var kind = modalKind.value;
if (kind === 'token' || kind === 'header') {
var value = stringField.value.trim();
if (!value) {
throw new Error(tKey('secrets.validation.value_required'));
}
return value;
}
if (kind === 'username_password') {
var username = usernameField.value.trim();
var password = passwordField.value;
if (!username || !password) {
throw new Error(tKey('secrets.validation.username_password_required'));
}
return {
username: username,
password: password,
};
}
try {
return JSON.parse(jsonField.value);
} catch (_error) {
throw new Error(tKey('secrets.validation.json_invalid'));
}
}
function renderSecrets() {
var usage = state.derived.usageBySecret;
var query = state.search.toLowerCase();
var rows = state.secrets.filter(function (secret) {
return !query || secret.name.toLowerCase().includes(query) || String(secret.kind || '').toLowerCase().includes(query);
});
summary.textContent = tfKey('secrets.active.subtitle', {
active: state.derived.activeSecretCount,
total: state.secrets.length,
});
window.CrankDom.clear(tbody);
if (cardList) {
window.CrankDom.clear(cardList);
}
if (state.loading && state.secrets.length === 0) {
var loadingRow = document.createElement('tr');
var loadingCell = document.createElement('td');
loadingCell.colSpan = 7;
loadingCell.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
loadingCell.textContent = tKey('secrets.loading');
loadingRow.appendChild(loadingCell);
tbody.appendChild(loadingRow);
renderSecretCards([], tKey('secrets.loading'));
return;
}
if (state.error) {
var errorRow = document.createElement('tr');
var errorCell = document.createElement('td');
errorCell.colSpan = 7;
errorCell.style.cssText = 'text-align:center;padding:36px;color:var(--danger,#f85149);';
errorCell.textContent = state.error;
errorRow.appendChild(errorCell);
tbody.appendChild(errorRow);
renderSecretCards([], state.error);
return;
}
if (!rows.length) {
var emptyRow = document.createElement('tr');
var emptyCell = document.createElement('td');
emptyCell.colSpan = 7;
emptyCell.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
emptyCell.textContent = state.secrets.length ? tKey('secrets.empty.search') : tKey('secrets.empty.none');
emptyRow.appendChild(emptyCell);
tbody.appendChild(emptyRow);
renderSecretCards(rows);
return;
}
rows.forEach(function (secret) {
var tr = document.createElement('tr');
var references = usage[secret.id] || [];
var nameCell = document.createElement('td');
nameCell.className = 'col-name';
nameCell.textContent = secret.name;
tr.appendChild(nameCell);
var kindCell = document.createElement('td');
kindCell.textContent = secretKindLabel(secret.kind);
tr.appendChild(kindCell);
var versionCell = document.createElement('td');
versionCell.className = 'col-mono';
versionCell.textContent = 'v' + secret.current_version;
tr.appendChild(versionCell);
var lastUsedCell = document.createElement('td');
lastUsedCell.textContent = secret.last_used_at ? formatDate(secret.last_used_at) : tKey('secrets.last_used.never');
tr.appendChild(lastUsedCell);
var usedByCell = document.createElement('td');
usedByCell.textContent = window.tPlural('secrets.used_by_count', references.length, { count: references.length });
tr.appendChild(usedByCell);
var statusCell = document.createElement('td');
var badge = document.createElement('span');
badge.className = 'badge ' + (secret.status === 'disabled' ? 'badge-revoked' : 'badge-active');
badge.textContent = tKey('secrets.status.' + secret.status);
statusCell.appendChild(badge);
tr.appendChild(statusCell);
var actions = document.createElement('td');
actions.className = 'col-actions';
var rotateButton = document.createElement('button');
rotateButton.type = 'button';
rotateButton.className = 'btn-secondary table-action-btn';
rotateButton.textContent = tKey('secrets.action.rotate');
rotateButton.setAttribute('data-testid', 'secret-rotate-action');
rotateButton.setAttribute('data-secret-id', secret.id);
rotateButton.addEventListener('click', function () {
openModal('rotate', secret);
});
actions.appendChild(rotateButton);
var deleteButton = document.createElement('button');
deleteButton.type = 'button';
deleteButton.className = 'btn-danger table-action-btn';
deleteButton.textContent = tKey('secrets.action.delete');
deleteButton.setAttribute('data-testid', 'secret-delete-action');
deleteButton.setAttribute('data-secret-id', secret.id);
deleteButton.addEventListener('click', async function () {
await deleteSecret(secret);
});
actions.appendChild(deleteButton);
tr.appendChild(actions);
tbody.appendChild(tr);
});
renderSecretCards(rows);
}
function renderSecretCards(rows, statusText) {
if (!cardList) {
return;
}
window.CrankDom.clear(cardList);
if (statusText && !rows.length) {
var messageCard = document.createElement('div');
messageCard.className = 'resource-card';
var message = document.createElement('div');
message.className = 'resource-meta-value';
message.textContent = statusText;
message.style.color = state.error ? 'var(--danger,#f85149)' : 'var(--text-muted)';
messageCard.appendChild(message);
cardList.appendChild(messageCard);
return;
}
rows.forEach(function(secret) {
var references = state.derived.usageBySecret[secret.id] || [];
var card = document.createElement('div');
card.className = 'resource-card';
var header = document.createElement('div');
header.className = 'resource-card-header';
var headerBody = document.createElement('div');
var title = document.createElement('div');
title.className = 'resource-card-title';
title.textContent = secret.name;
var subtitle = document.createElement('div');
subtitle.className = 'resource-card-subtitle';
subtitle.textContent = secretKindLabel(secret.kind);
headerBody.appendChild(title);
headerBody.appendChild(subtitle);
var actions = document.createElement('div');
actions.className = 'resource-card-actions';
var badge = document.createElement('span');
badge.className = 'badge ' + (secret.status === 'disabled' ? 'badge-revoked' : 'badge-active');
badge.textContent = tKey('secrets.status.' + secret.status);
actions.appendChild(badge);
header.appendChild(headerBody);
header.appendChild(actions);
card.appendChild(header);
var metaGrid = document.createElement('div');
metaGrid.className = 'resource-meta-grid';
metaGrid.appendChild(buildSecretMetaItem('secrets.th.version', 'v' + secret.current_version));
metaGrid.appendChild(buildSecretMetaItem(
'secrets.th.last_used',
secret.last_used_at ? formatDate(secret.last_used_at) : tKey('secrets.last_used.never')
));
metaGrid.appendChild(buildSecretMetaItem(
'secrets.th.used_by',
window.tPlural('secrets.used_by_count', references.length, { count: references.length })
));
card.appendChild(metaGrid);
var actionRow = document.createElement('div');
actionRow.className = 'resource-card-actions';
var rotateButton = document.createElement('button');
rotateButton.className = 'btn-secondary';
rotateButton.type = 'button';
rotateButton.textContent = tKey('secrets.action.rotate');
rotateButton.addEventListener('click', function() {
openModal('rotate', secret);
});
actionRow.appendChild(rotateButton);
var deleteButton = document.createElement('button');
deleteButton.className = 'btn-secondary';
deleteButton.type = 'button';
deleteButton.textContent = tKey('secrets.action.delete');
deleteButton.addEventListener('click', async function() {
await deleteSecret(secret);
});
actionRow.appendChild(deleteButton);
card.appendChild(actionRow);
cardList.appendChild(card);
});
}
function buildSecretMetaItem(labelKey, valueText) {
var item = document.createElement('div');
item.className = 'resource-meta-item';
var label = document.createElement('div');
label.className = 'resource-meta-label';
label.textContent = tKey(labelKey);
var value = document.createElement('div');
value.className = 'resource-meta-value';
value.textContent = valueText;
item.appendChild(label);
item.appendChild(value);
return item;
}
function renderProfiles() {
var usage = state.derived.usageBySecret;
var secretsById = state.derived.secretsById;
profilesSummary.textContent = window.tPlural(
'secrets.profiles.subtitle_count',
state.profiles.length,
{ count: state.profiles.length }
);
window.CrankDom.clear(profilesList);
if (state.error) {
profilesList.appendChild(window.CrankDom.createEmptyState(
tKey('secrets.profiles.error_title'),
state.error
));
return;
}
if (state.loading && state.profiles.length === 0) {
profilesList.appendChild(window.CrankDom.createEmptyState(
tKey('secrets.profiles.loading_title'),
tKey('secrets.profiles.loading_body')
));
return;
}
if (!state.profiles.length) {
profilesList.appendChild(window.CrankDom.createEmptyState(
tKey('secrets.profiles.empty_title'),
tKey('secrets.profiles.empty_body')
));
return;
}
state.profiles.forEach(function (profile) {
var secretIds = secretIdsForProfile(profile);
var card = document.createElement('div');
card.className = 'resource-card';
var header = document.createElement('div');
header.className = 'resource-card-header';
var headerBody = document.createElement('div');
var title = document.createElement('div');
title.className = 'resource-card-title';
title.textContent = profile.name;
headerBody.appendChild(title);
var subtitle = document.createElement('div');
subtitle.className = 'resource-card-subtitle';
subtitle.textContent = profileSummary(profile, secretsById);
headerBody.appendChild(subtitle);
var pillRow = document.createElement('div');
pillRow.className = 'resource-pill-row';
var statusPill = document.createElement('span');
statusPill.className = 'resource-status-pill active';
statusPill.textContent = authKindLabel(profile.kind);
pillRow.appendChild(statusPill);
headerBody.appendChild(pillRow);
header.appendChild(headerBody);
card.appendChild(header);
var metaGrid = document.createElement('div');
metaGrid.className = 'resource-meta-grid';
[
[tKey('secrets.profiles.meta.created'), formatDate(profile.created_at)],
[tKey('secrets.profiles.meta.updated'), formatDate(profile.updated_at)]
].forEach(function(entry) {
var item = document.createElement('div');
item.className = 'resource-meta-item';
var label = document.createElement('div');
label.className = 'resource-meta-label';
label.textContent = entry[0];
var value = document.createElement('div');
value.className = 'resource-meta-value';
value.textContent = entry[1];
item.appendChild(label);
item.appendChild(value);
metaGrid.appendChild(item);
});
card.appendChild(metaGrid);
var detail = document.createElement('div');
detail.className = 'resource-detail-block';
var detailTitle = document.createElement('div');
detailTitle.className = 'resource-detail-title';
detailTitle.textContent = tKey('secrets.profiles.references');
detail.appendChild(detailTitle);
var refList = document.createElement('div');
refList.className = 'secret-ref-list';
secretIds.forEach(function(secretId) {
var users = usage[secretId] || [];
var pill = document.createElement('span');
pill.className = 'secret-ref-pill';
var strong = document.createElement('strong');
strong.textContent = secretName(secretsById, secretId);
var count = document.createElement('span');
count.textContent = tfKey('secrets.profiles.reference_count', { count: users.length });
pill.appendChild(strong);
pill.appendChild(count);
refList.appendChild(pill);
});
detail.appendChild(refList);
card.appendChild(detail);
profilesList.appendChild(card);
});
}
async function load() {
state.workspaceId = currentWorkspaceId();
state.loading = true;
state.error = '';
renderSecrets();
renderProfiles();
if (!state.workspaceId) {
state.secrets = [];
state.profiles = [];
recomputeDerivedState();
state.loading = false;
state.error = tKey('secrets.error.workspace');
renderSecrets();
renderProfiles();
return;
}
try {
var results = await Promise.all([
window.CrankApi.listSecrets(state.workspaceId),
window.CrankApi.listAuthProfiles(state.workspaceId),
]);
state.secrets = (results[0] && results[0].items) || [];
state.profiles = (results[1] && results[1].items) || [];
recomputeDerivedState();
} catch (error) {
state.error = error.message || tKey('secrets.error.load');
state.secrets = [];
state.profiles = [];
recomputeDerivedState();
} finally {
state.loading = false;
renderSecrets();
renderProfiles();
}
}
async function deleteSecret(secret) {
if (!confirm(tfKey('secrets.confirm.delete', { name: secret.name }))) {
return;
}
try {
await window.CrankApi.deleteSecret(state.workspaceId, secret.id);
await load();
if (window.CrankUi) {
window.CrankUi.success(
tfKey('secrets.toast.delete_message', { name: secret.name }),
tKey('secrets.toast.delete_title')
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || tKey('secrets.toast.delete_error_message'),
tKey('secrets.toast.delete_error_title')
);
}
}
}
async function submitModal() {
var kind = modalKind.value;
var value = buildSecretValue();
modalSubmit.disabled = true;
modalSubmit.textContent = state.modalMode === 'rotate'
? tKey('secrets.modal.rotating')
: tKey('secrets.modal.creating');
try {
if (state.modalMode === 'rotate') {
await window.CrankApi.rotateSecret(state.workspaceId, state.modalSecretId, { value: value });
if (window.CrankUi) {
window.CrankUi.success(
tKey('secrets.toast.rotate_message'),
tKey('secrets.toast.rotate_title')
);
}
} else {
var name = modalName.value.trim();
if (!name) {
throw new Error(tKey('secrets.validation.name_required'));
}
await window.CrankApi.createSecret(state.workspaceId, {
name: name,
kind: kind,
value: value,
});
if (window.CrankUi) {
window.CrankUi.success(
tKey('secrets.toast.create_message'),
tKey('secrets.toast.create_title')
);
}
}
closeModal();
await load();
} catch (error) {
if (window.CrankUi) {
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');
}
}
document.getElementById('btn-create-secret').addEventListener('click', function () {
openModal('create');
});
document.getElementById('secret-modal-close-btn').addEventListener('click', closeModal);
document.getElementById('secret-modal-cancel-btn').addEventListener('click', closeModal);
modal.addEventListener('click', function (event) {
if (event.target === modal) {
closeModal();
}
});
modalKind.addEventListener('change', updateKindFields);
modalSubmit.addEventListener('click', function () {
void submitModal();
});
searchInput.addEventListener('input', function (event) {
state.search = event.target.value || '';
renderSecrets();
});
document.addEventListener('workspace:changed', function () {
void load();
});
updateKindFields();
void load();
}
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') {
window.CrankDiagnostics.bootstrap('secrets', initSecretsPage);
} else {
document.addEventListener('DOMContentLoaded', initSecretsPage);
}
+400
View File
@@ -0,0 +1,400 @@
var settingsSession = null;
var settingsCapabilities = null;
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : key;
}
function settingsWorkspaceId() {
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
return workspace ? workspace.id : null;
}
function initials(displayName, email) {
var source = displayName || email || 'Crank';
return source
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map(function(part) { return part.charAt(0).toUpperCase(); })
.join('') || 'CR';
}
function setStatus(id, text, isError) {
var node = document.getElementById(id);
if (!node) return;
node.textContent = text || '';
node.style.color = text
? (isError ? 'var(--red)' : 'var(--green)')
: 'var(--text-muted)';
}
function titleCaseRole(role) {
var normalized = String(role || 'viewer').toLowerCase();
if (normalized === 'operator') return tKey('workspace_setup.role.operator');
if (normalized === 'owner') return tKey('workspace_setup.role.owner');
if (normalized === 'admin') return tKey('workspace_setup.role.admin');
if (normalized === 'viewer') return tKey('workspace_setup.role.viewer');
return normalized.replace(/^\w/, function(character) {
return character.toUpperCase();
});
}
function populateCurrentSession(session) {
var summary = document.getElementById('settings-session-summary');
if (!summary) {
return;
}
if (!(session && session.user)) {
summary.textContent = tKey('settings.session.unavailable');
return;
}
var membership = null;
if (Array.isArray(session.memberships)) {
membership = session.memberships.find(function(item) {
return item.workspace && item.workspace.id === session.current_workspace_id;
}) || session.memberships[0] || null;
}
var workspaceLabel = membership && membership.workspace
? (membership.workspace.display_name || membership.workspace.slug || membership.workspace.id)
: tKey('settings.session.current_workspace');
var roleLabel = membership ? titleCaseRole(membership.role) : tKey('workspace_setup.role.viewer');
summary.textContent = tfKey('settings.session.summary', {
email: session.user.email,
role: roleLabel,
workspace: workspaceLabel,
});
}
function capabilityLabel(key) {
return tKey('settings.capability.' + key);
}
function populateNotificationsCapabilityHint(capabilities) {
var title = document.getElementById('settings-notifications-capability-title');
var body = document.getElementById('settings-notifications-capability-body');
if (!title || !body || !capabilities) {
return;
}
title.textContent = tKey('settings.notifications.capability_title_' + capabilities.edition);
body.textContent = tfKey('settings.notifications.capability_body_' + capabilities.edition, {
edition: capabilities.edition,
});
}
function populateCapabilities(capabilities) {
var title = document.getElementById('settings-security-capability-title');
var body = document.getElementById('settings-security-capability-body');
if (!title || !body || !capabilities) {
return;
}
var protocols = (capabilities.supported_protocols || []).map(capabilityLabel).join(', ');
var accessModes = (capabilities.machine_access_modes || []).map(capabilityLabel).join(', ');
var securityLevels = (capabilities.supported_security_levels || []).map(capabilityLabel).join(', ');
var limits = capabilities.limits || {};
title.textContent = tKey('settings.security.capability_title_' + capabilities.edition);
body.textContent = tfKey('settings.security.capability_summary', {
protocols: protocols || capabilityLabel('none'),
access_modes: accessModes || capabilityLabel('none'),
security_levels: securityLevels || capabilityLabel('none'),
max_workspaces: limits.max_workspaces == null ? capabilityLabel('unlimited') : String(limits.max_workspaces),
max_users: limits.max_users_per_workspace == null ? capabilityLabel('unlimited') : String(limits.max_users_per_workspace),
max_agents: limits.max_agents_per_workspace == null ? capabilityLabel('unlimited') : String(limits.max_agents_per_workspace),
});
populateNotificationsCapabilityHint(capabilities);
}
function populateProfile(session) {
if (!session || !session.user) {
return;
}
var user = session.user;
document.getElementById('profile-avatar').textContent = initials(user.display_name, user.email);
document.getElementById('profile-display-name').textContent = user.display_name || 'Crank';
document.getElementById('profile-email').textContent = user.email || '';
var firstName = document.getElementById('field-firstname');
var email = document.getElementById('field-email');
if (firstName) {
firstName.value = user.display_name || '';
}
if (email) {
email.value = user.email || '';
}
populateCurrentSession(session);
}
async function loadProfile() {
if (!window.CrankAuth || !window.CrankApi) {
return;
}
settingsSession = await window.CrankAuth.fetchSession(true);
populateProfile(settingsSession);
}
async function loadCapabilities() {
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
return;
}
try {
settingsCapabilities = await window.CrankApi.getCapabilities();
populateCapabilities(settingsCapabilities);
} catch (_error) {
}
}
async function renderOverlaySlots() {
if (!window.CrankOverlay || typeof window.CrankOverlay.render !== 'function') {
return;
}
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
await window.CrankOverlay.render(document, {
workspace: window.getCurrentWorkspace ? window.getCurrentWorkspace() : null,
capabilities: settingsCapabilities,
locale: localStorage.getItem('crank_lang') || 'en',
});
}
function bindProfileSave() {
var button = document.getElementById('settings-profile-save-btn');
if (!button || button.dataset.bound === 'true') {
return;
}
button.dataset.bound = 'true';
button.addEventListener('click', async function() {
var original = button.textContent;
button.disabled = true;
button.textContent = tKey('settings.profile.saving');
setStatus('settings-profile-status', '', false);
try {
var session = await window.CrankApi.updateProfile({
display_name: document.getElementById('field-firstname').value.trim(),
email: document.getElementById('field-email').value.trim(),
});
settingsSession = session;
if (window.CrankAuth && typeof window.CrankAuth.replaceSession === 'function') {
window.CrankAuth.replaceSession(session);
}
populateProfile(session);
setStatus('settings-profile-status', tKey('settings.profile.saved_status'), false);
button.textContent = tKey('settings.profile.saved');
} catch (error) {
setStatus('settings-profile-status', error.message || tKey('settings.profile.save_error'), true);
button.textContent = original;
} finally {
setTimeout(function() {
button.disabled = false;
button.textContent = original;
}, 1200);
}
});
}
function bindPasswordSave() {
var button = document.getElementById('settings-password-save-btn');
if (!button || button.dataset.bound === 'true') {
return;
}
button.dataset.bound = 'true';
button.addEventListener('click', async function() {
var currentPassword = document.getElementById('security-current-password').value;
var newPassword = document.getElementById('security-new-password').value;
var confirmPassword = document.getElementById('security-confirm-password').value;
if (newPassword !== confirmPassword) {
setStatus('settings-password-status', tKey('settings.security.mismatch'), true);
return;
}
var original = button.textContent;
button.disabled = true;
button.textContent = tKey('settings.profile.saving');
setStatus('settings-password-status', '', false);
try {
await window.CrankApi.changePassword({
current_password: currentPassword,
new_password: newPassword,
});
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);
button.textContent = tKey('settings.profile.saved');
} catch (error) {
setStatus('settings-password-status', error.message || tKey('settings.security.save_error'), true);
button.textContent = original;
} finally {
setTimeout(function() {
button.disabled = false;
button.textContent = original;
}, 1200);
}
});
}
function injectLanguageSwitcher() {
var profileSection = document.getElementById('section-profile');
if (!profileSection) {
return;
}
var cardBody = profileSection.querySelector('.section-card-body');
if (!cardBody) {
return;
}
fetch('html/fragments/lang-switcher.html')
.then(function(response) { return response.text(); })
.then(function(html) {
var template = document.createElement('template');
template.innerHTML = html;
var fragment = template.content.cloneNode(true);
var saveRow = cardBody.querySelector('div[style*="justify-content:flex-end"]');
if (saveRow) {
saveRow.parentNode.insertBefore(fragment, saveRow);
} else {
cardBody.appendChild(fragment);
}
if (typeof applyLang === 'function') {
applyLang();
}
});
}
function bindSectionNavigation() {
var navItems = document.querySelectorAll('.settings-nav-item[data-section]');
navItems.forEach(function (button) {
button.addEventListener('click', function () {
navItems.forEach(function (item) { item.classList.remove('active'); });
this.classList.add('active');
var target = document.getElementById('section-' + this.dataset.section);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
var initialSection = window.location.hash ? window.location.hash.slice(1) : 'profile';
var initialButton = document.querySelector('.settings-nav-item[data-section="' + initialSection + '"]')
|| document.querySelector('.settings-nav-item[data-section="profile"]');
if (initialButton) {
setTimeout(function () { initialButton.click(); }, 50);
}
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) {
return;
}
var section = entry.target.id.replace('section-', '');
document.querySelectorAll('.settings-nav-item[data-section]').forEach(function (button) {
button.classList.toggle('active', button.dataset.section === section);
});
});
}, { threshold: 0.3 });
document.querySelectorAll('[id^="section-"]').forEach(function (section) {
if (!section.hidden) {
observer.observe(section);
}
});
}
async function loadWorkspaceSettings() {
if (!window.CrankApi || !window.whenWorkspacesReady) {
return;
}
await window.whenWorkspacesReady();
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
if (!workspace) {
return;
}
try {
var record = await window.CrankApi.getWorkspace(workspace.id);
var item = record.workspace;
var settings = item.settings || {};
document.getElementById('settings-ws-slug').value = item.slug || '';
document.getElementById('settings-ws-display-name').value = item.display_name || '';
document.getElementById('settings-ws-description').value = settings.description || '';
var saveButton = document.getElementById('settings-ws-save-btn');
if (saveButton.dataset.bound === 'true') {
return;
}
saveButton.dataset.bound = 'true';
saveButton.addEventListener('click', async function () {
var saveButton = this;
var original = saveButton.textContent;
saveButton.disabled = true;
saveButton.textContent = tKey('settings.workspace.saving');
try {
await window.CrankApi.updateWorkspace(item.id, {
slug: document.getElementById('settings-ws-slug').value.trim(),
display_name: document.getElementById('settings-ws-display-name').value.trim(),
settings: {
description: document.getElementById('settings-ws-description').value.trim(),
color: (workspace.settings && workspace.settings.color) || '#0d9488',
},
});
if (window.refreshWorkspaces) {
var workspaces = await window.refreshWorkspaces();
var updated = workspaces.find(function (entry) { return entry.id === item.id; });
if (updated && window.setCurrentWorkspace) {
await window.setCurrentWorkspace(updated);
}
}
saveButton.textContent = tKey('settings.workspace.saved');
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('settings.workspace.save_error'), tKey('settings.workspace.save_error_title'));
}
saveButton.textContent = original;
} finally {
setTimeout(function () {
saveButton.disabled = false;
saveButton.textContent = original;
}, 1200);
}
});
} catch (_error) {
}
}
async function initSettingsPage() {
injectLanguageSwitcher();
bindSectionNavigation();
await loadProfile();
await loadCapabilities();
bindProfileSave();
bindPasswordSave();
await loadWorkspaceSettings();
await renderOverlaySlots();
}
document.addEventListener('DOMContentLoaded', function () {
void initSettingsPage();
});
+55
View File
@@ -0,0 +1,55 @@
(function() {
var allowedSlots = {
'settings.sso_panel': true,
'settings.totp_panel': true,
'settings.audit_panel': true,
'settings.billing_panel': true,
'wizard.protocol_cards.graphql': true,
'wizard.protocol_cards.grpc': true,
'wizard.protocol_cards.soap': true,
'wizard.protocol_cards.websocket': true,
};
var handlers = Object.create(null);
function noop() {}
function isAllowed(slotId) {
return !!allowedSlots[slotId];
}
function register(slotId, render) {
if (!isAllowed(slotId) || typeof render !== 'function') {
return false;
}
handlers[slotId] = render;
return true;
}
function renderInto(target, slotId, context) {
if (!(target && slotId && isAllowed(slotId))) {
return;
}
var handler = handlers[slotId] || noop;
handler(target, context || {});
}
function renderAll(root, context) {
var scope = root || document;
scope.querySelectorAll('[data-slot]').forEach(function(target) {
renderInto(target, target.getAttribute('data-slot'), context);
});
}
function knownSlots() {
return Object.keys(allowedSlots);
}
window.CrankUiSlots = {
isAllowed: isAllowed,
knownSlots: knownSlots,
register: register,
renderAll: renderAll,
renderInto: renderInto,
};
}());
+105
View File
@@ -0,0 +1,105 @@
(function() {
var CONTAINER_ID = 'crank-toast-container';
var nextToastId = 0;
function ensureContainer() {
var container = document.getElementById(CONTAINER_ID);
if (container) return container;
container = document.createElement('div');
container.id = CONTAINER_ID;
container.className = 'toast-stack';
document.body.appendChild(container);
return container;
}
function dismiss(toast) {
if (!toast || !toast.parentNode) return;
toast.classList.add('closing');
window.setTimeout(function() {
if (toast.parentNode) toast.parentNode.removeChild(toast);
}, 180);
}
function notify(options) {
var config = options || {};
var duration = config.duration === 0 ? 0 : (config.duration || 3600);
var toast = document.createElement('div');
var copy = document.createElement('div');
var close = document.createElement('button');
var closeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
var lineA = document.createElementNS('http://www.w3.org/2000/svg', 'line');
var lineB = document.createElementNS('http://www.w3.org/2000/svg', 'line');
toast.id = 'toast_' + (++nextToastId);
toast.className = 'toast-card toast-' + (config.type || 'info');
copy.className = 'toast-copy';
if (config.title) {
var title = document.createElement('div');
title.className = 'toast-title';
title.textContent = config.title;
copy.appendChild(title);
}
if (config.message) {
var message = document.createElement('div');
message.className = 'toast-message';
message.textContent = config.message;
copy.appendChild(message);
}
close.className = 'toast-close';
close.type = 'button';
close.setAttribute('aria-label', 'Dismiss');
closeIcon.setAttribute('width', '12');
closeIcon.setAttribute('height', '12');
closeIcon.setAttribute('viewBox', '0 0 12 12');
closeIcon.setAttribute('fill', 'none');
closeIcon.setAttribute('stroke', 'currentColor');
closeIcon.setAttribute('stroke-width', '1.8');
closeIcon.setAttribute('stroke-linecap', 'round');
lineA.setAttribute('x1', '1');
lineA.setAttribute('y1', '1');
lineA.setAttribute('x2', '11');
lineA.setAttribute('y2', '11');
lineB.setAttribute('x1', '11');
lineB.setAttribute('y1', '1');
lineB.setAttribute('x2', '1');
lineB.setAttribute('y2', '11');
closeIcon.appendChild(lineA);
closeIcon.appendChild(lineB);
close.appendChild(closeIcon);
toast.appendChild(copy);
toast.appendChild(close);
close.addEventListener('click', function() {
dismiss(toast);
});
ensureContainer().appendChild(toast);
if (duration > 0) {
window.setTimeout(function() {
dismiss(toast);
}, duration);
}
return toast.id;
}
window.CrankUi = {
notify: notify,
success: function(message, title) {
return notify({ type: 'success', title: title || 'Done', message: message });
},
error: function(message, title) {
return notify({ type: 'error', title: title || 'Request failed', message: message });
},
info: function(message, title) {
return notify({ type: 'info', title: title || 'Info', message: message });
}
};
}());
+549
View File
@@ -0,0 +1,549 @@
document.addEventListener('DOMContentLoaded', function () {
var state = {
period: '7d',
workspaceId: null,
usage: null,
derived: {
localizedOperations: [],
normalizedTimeline: [],
timelineMaxValue: 0,
totalCalls: 0,
},
loading: false,
loadError: '',
};
var periodSelect = document.getElementById('period');
var exportBtn = document.querySelector('.page-header-actions .btn-secondary');
var chartWrap = document.getElementById('chart-bars');
var tableBody = document.getElementById('usage-tbody');
var cardList = document.getElementById('usage-card-list');
var chartTemplate = document.getElementById('tmpl-chart-bar');
var rowTemplate = document.getElementById('tmpl-usage-row');
var subtitle = document.querySelector('.section-card-subtitle');
var statCards = document.querySelectorAll('.stats-grid .stat-card');
function tKey(key) {
return window.t ? t(key) : key;
}
function tfKey(key, vars) {
return window.tf ? tf(key, vars) : tKey(key);
}
function currentLocale() {
return localStorage.getItem('crank_lang') === 'ru' ? 'ru-RU' : 'en-US';
}
function currentWorkspaceId() {
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
return workspace ? workspace.id : null;
}
function element(tag, className, text) {
var node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined && text !== null) node.textContent = text;
return node;
}
function formatCount(value) {
return Number(value || 0).toLocaleString(currentLocale());
}
function formatMs(value) {
if (!value) {
return '0ms';
}
if (value >= 1000) {
return (value / 1000).toFixed(1) + 's';
}
return value + 'ms';
}
function periodLabel(period) {
return tKey('usage.period.label.' + period);
}
function protocolColor(protocol) {
return 'var(--blue)';
}
function protocolLabel(protocol) {
return 'REST';
}
function localizedUsageOperation(operation) {
if (!window.localizeDemoOperation) {
return operation;
}
return window.localizeDemoOperation({
name: operation.operation_name,
operation_display_name: operation.operation_display_name,
description: operation.operation_description || null,
});
}
function chartBucketLabel(timestamp, period, index) {
var date = new Date(timestamp);
if (period === '7d') {
return date.toLocaleDateString(currentLocale(), { weekday: 'short' });
}
if (period === '90d') {
return date.toLocaleDateString(currentLocale(), { month: 'short' });
}
return tfKey('usage.chart.week', { index: index + 1 });
}
function toUtcBucketStart(date, period) {
var bucket = new Date(date.getTime());
bucket.setUTCMilliseconds(0);
bucket.setUTCSeconds(0);
bucket.setUTCMinutes(0);
bucket.setUTCHours(0);
if (period === '30d' || period === 'this_month') {
var day = bucket.getUTCDay();
var offset = day === 0 ? 6 : (day - 1);
bucket.setUTCDate(bucket.getUTCDate() - offset);
return bucket;
}
if (period === '90d') {
bucket.setUTCDate(1);
return bucket;
}
return bucket;
}
function shiftUtcBucket(date, period, amount) {
var shifted = new Date(date.getTime());
if (period === '30d' || period === 'this_month') {
shifted.setUTCDate(shifted.getUTCDate() + (amount * 7));
return shifted;
}
if (period === '90d') {
shifted.setUTCMonth(shifted.getUTCMonth() + amount);
return shifted;
}
shifted.setUTCDate(shifted.getUTCDate() + amount);
return shifted;
}
function bucketKey(date) {
return date.toISOString().slice(0, 19) + 'Z';
}
function expectedBucketCount(period, endBucket) {
if (period === '7d') {
return 7;
}
if (period === '30d') {
return 5;
}
if (period === '90d') {
return 4;
}
if (period === 'this_month') {
var firstDay = new Date(Date.UTC(endBucket.getUTCFullYear(), endBucket.getUTCMonth(), 1));
var firstWeek = toUtcBucketStart(firstDay, period);
var diffMs = endBucket.getTime() - firstWeek.getTime();
return Math.max(1, Math.floor(diffMs / (7 * 24 * 60 * 60 * 1000)) + 1);
}
return 7;
}
function normalizeTimeline(timeline, period) {
if (!timeline.length) {
return [];
}
var byBucket = {};
timeline.forEach(function(point) {
byBucket[point.bucket_start] = point;
});
var latestBucket = timeline.reduce(function(current, point) {
return point.bucket_start > current.bucket_start ? point : current;
}, timeline[0]);
var endBucket = toUtcBucketStart(new Date(latestBucket.bucket_start), period);
var bucketCount = expectedBucketCount(period, endBucket);
var startBucket = shiftUtcBucket(endBucket, period, -(bucketCount - 1));
var normalized = [];
for (var index = 0; index < bucketCount; index += 1) {
var bucketDate = shiftUtcBucket(startBucket, period, index);
var key = bucketKey(bucketDate);
normalized.push(byBucket[key] || {
bucket_start: key,
calls_ok: 0,
calls_error: 0,
});
}
return normalized;
}
function recomputeDerivedState() {
var operations = state.usage ? (state.usage.operations || []) : [];
var timeline = state.usage ? normalizeTimeline(state.usage.timeline || [], state.period) : [];
var totalCalls = operations.reduce(function (sum, operation) {
return sum + operation.calls_total;
}, 0);
var timelineMaxValue = timeline.reduce(function (maxValue, point) {
return Math.max(maxValue, point.calls_ok + point.calls_error);
}, 0);
state.derived = {
localizedOperations: operations.map(function (operation) {
return {
operation: operation,
localized: localizedUsageOperation(operation),
};
}),
normalizedTimeline: timeline,
timelineMaxValue: timelineMaxValue,
totalCalls: totalCalls,
};
}
function buildEmptyState(title, message, compact) {
var empty = element('div', 'empty-state');
if (compact) {
empty.style.padding = '0';
}
empty.appendChild(element('div', 'empty-state-title', title));
empty.appendChild(element('div', 'empty-state-text', message));
return empty;
}
function renderEmpty(title, message) {
chartWrap.innerHTML = '';
var chartEmpty = buildEmptyState(title, message, false);
chartEmpty.style.width = '100%';
chartWrap.appendChild(chartEmpty);
tableBody.innerHTML = '';
var row = document.createElement('tr');
var cell = document.createElement('td');
cell.colSpan = 7;
cell.style.padding = '24px';
cell.appendChild(buildEmptyState(title, message, true));
row.appendChild(cell);
tableBody.appendChild(row);
}
function renderStats() {
var summary = state.usage ? state.usage.summary : null;
var cards = [
{
value: formatCount(summary ? summary.rollup.calls_total : 0),
text: tfKey('usage.stats.across', { period: periodLabel(state.period) }),
},
{
value: ((summary ? summary.success_rate : 0) || 0).toFixed(1) + '%',
text: tfKey('usage.stats.success_calls', { count: formatCount(summary ? summary.rollup.calls_ok : 0) }),
},
{
value: formatMs(summary ? summary.rollup.p50_ms : 0),
text: tKey('usage.stats.median'),
},
{
value: formatMs(summary ? summary.rollup.p99_ms : 0),
text: tfKey('usage.stats.error_calls', { count: formatCount(summary ? summary.rollup.calls_error : 0) }),
}
];
cards.forEach(function (card, index) {
var node = statCards[index];
if (!node) {
return;
}
node.querySelector('.stat-value').textContent = card.value;
var delta = node.querySelector('.stat-delta');
delta.className = 'stat-delta';
delta.textContent = card.text;
});
}
function renderChart() {
var timeline = state.derived.normalizedTimeline;
chartWrap.innerHTML = '';
if (!timeline.length) {
var chartEmpty = buildEmptyState(
tKey('usage.chart.empty.title'),
tKey('usage.chart.empty.sub'),
false
);
chartEmpty.style.width = '100%';
chartWrap.appendChild(chartEmpty);
return;
}
var maxValue = state.derived.timelineMaxValue;
timeline.forEach(function (point, index) {
var node = chartTemplate.content.cloneNode(true);
var okHeight = maxValue ? Math.round((point.calls_ok / maxValue) * 160) : 0;
var errorHeight = maxValue ? Math.round((point.calls_error / maxValue) * 160) : 0;
if (point.calls_ok > 0) {
okHeight = Math.max(okHeight, 6);
}
if (point.calls_error > 0) {
errorHeight = Math.max(errorHeight, 6);
}
node.querySelector('.chart-bar.success').style.height = okHeight + 'px';
node.querySelector('.chart-bar.success').dataset.tip = tfKey('usage.chart.ok', { count: formatCount(point.calls_ok) });
node.querySelector('.chart-bar.error').style.height = errorHeight + 'px';
node.querySelector('.chart-bar.error').dataset.tip = tfKey('usage.chart.errors', { count: formatCount(point.calls_error) });
node.querySelector('.chart-col-label').textContent = chartBucketLabel(point.bucket_start, state.period, index);
chartWrap.appendChild(node);
});
}
function renderTable() {
var operations = state.derived.localizedOperations;
tableBody.innerHTML = '';
if (cardList) {
cardList.innerHTML = '';
}
if (!operations.length) {
var row = document.createElement('tr');
var cell = document.createElement('td');
cell.colSpan = 7;
cell.style.padding = '24px';
cell.appendChild(
buildEmptyState(
tKey('usage.table.empty.title'),
tKey('usage.table.empty.sub'),
true
)
);
row.appendChild(cell);
tableBody.appendChild(row);
renderOperationCards([]);
return;
}
operations.forEach(function (entry) {
var operation = entry.operation;
var localizedOperation = entry.localized;
var node = rowTemplate.content.cloneNode(true);
var errorRate = operation.calls_total === 0
? 0
: ((operation.calls_error / operation.calls_total) * 100);
var share = state.derived.totalCalls === 0
? 0
: ((operation.calls_total / state.derived.totalCalls) * 100);
node.querySelector('.col-name').textContent = localizedOperation.operation_display_name || operation.operation_display_name;
node.querySelector('.col-op-slug').textContent = operation.operation_name;
var proto = node.querySelector('.col-proto');
proto.textContent = protocolLabel(operation.protocol);
proto.style.color = protocolColor(operation.protocol);
node.querySelector('.col-calls').textContent = formatCount(operation.calls_total);
node.querySelector('.col-errors').textContent = formatCount(operation.calls_error);
var errorRateEl = node.querySelector('.col-err-rate');
errorRateEl.textContent = errorRate.toFixed(2) + '%';
errorRateEl.style.color = errorRate > 5 ? 'var(--red)' : (errorRate > 1 ? 'var(--amber)' : 'var(--green)');
var latencyText = node.querySelector('.col-latency-text');
latencyText.textContent =
formatMs(operation.p50_ms) + ' / ' + formatMs(operation.p95_ms) + ' / ' + formatMs(operation.p99_ms);
var base = Math.max(operation.p99_ms, 1);
node.querySelector('.latency-p50').style.width = Math.max(4, Math.round((operation.p50_ms / base) * 64)) + 'px';
node.querySelector('.latency-p95').style.width = Math.max(4, Math.round(((operation.p95_ms - operation.p50_ms) / base) * 64)) + 'px';
node.querySelector('.latency-p99').style.width = Math.max(4, Math.round(((operation.p99_ms - operation.p95_ms) / base) * 64)) + 'px';
node.querySelector('.col-quota-label').textContent = tfKey('usage.table.share', { value: share.toFixed(1) });
node.querySelector('.col-quota-fill').style.width = share.toFixed(1) + '%';
tableBody.appendChild(node);
});
renderOperationCards(operations);
}
function renderOperationCards(operations) {
if (!cardList) {
return;
}
cardList.innerHTML = '';
if (!operations.length) {
cardList.appendChild(buildEmptyState(
tKey('usage.table.empty.title'),
tKey('usage.table.empty.sub'),
false
));
return;
}
operations.forEach(function(entry) {
var operation = entry.operation;
var localizedOperation = entry.localized;
var errorRate = operation.calls_total === 0
? 0
: ((operation.calls_error / operation.calls_total) * 100);
var share = state.derived.totalCalls === 0
? 0
: ((operation.calls_total / state.derived.totalCalls) * 100);
var card = element('div', 'resource-card');
var header = element('div', 'resource-card-header');
var headerMain = element('div');
headerMain.appendChild(element(
'div',
'resource-card-title',
localizedOperation.operation_display_name || operation.operation_display_name
));
headerMain.appendChild(element('div', 'resource-card-subtitle', operation.operation_name));
var actions = element('div', 'resource-card-actions');
var protocolBadge = element('span', 'badge');
protocolBadge.textContent = protocolLabel(operation.protocol);
protocolBadge.style.color = protocolColor(operation.protocol);
actions.appendChild(protocolBadge);
header.appendChild(headerMain);
header.appendChild(actions);
card.appendChild(header);
var metaGrid = element('div', 'resource-meta-grid');
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.calls', formatCount(operation.calls_total)));
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.errors', formatCount(operation.calls_error)));
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.error_rate', errorRate.toFixed(2) + '%'));
metaGrid.appendChild(buildUsageMetaItem(
'usage.table.th.latency',
formatMs(operation.p50_ms) + ' / ' + formatMs(operation.p95_ms) + ' / ' + formatMs(operation.p99_ms)
));
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.share', tfKey('usage.table.share', { value: share.toFixed(1) })));
card.appendChild(metaGrid);
cardList.appendChild(card);
});
}
function buildUsageMetaItem(labelKey, valueText) {
var item = element('div', 'resource-meta-item');
item.appendChild(element('div', 'resource-meta-label', tKey(labelKey)));
item.appendChild(element('div', 'resource-meta-value', valueText));
return item;
}
function renderUsage() {
if (state.loading && !state.usage) {
renderEmpty(tKey('usage.loading.title'), tKey('usage.loading.sub'));
return;
}
if (state.loadError) {
renderEmpty(tKey('usage.error.title'), state.loadError);
return;
}
renderStats();
renderChart();
renderTable();
if (subtitle) {
subtitle.textContent = tfKey('usage.table.subtitle', { period: periodLabel(state.period) });
}
}
async function loadUsage() {
if (!window.CrankApi) {
state.loadError = tKey('usage.error.api');
renderUsage();
return;
}
state.workspaceId = currentWorkspaceId();
if (!state.workspaceId) {
state.loadError = tKey('usage.error.workspace');
renderUsage();
return;
}
state.loading = true;
state.loadError = '';
renderUsage();
try {
state.usage = await window.CrankApi.getUsageOverview(state.workspaceId, { period: state.period });
recomputeDerivedState();
} catch (error) {
state.loadError = error.message || tKey('usage.error.load');
state.usage = null;
recomputeDerivedState();
} finally {
state.loading = false;
renderUsage();
}
}
function exportCsv() {
if (!state.usage) {
if (window.CrankUi) {
window.CrankUi.info(tKey('usage.export.empty.body'), tKey('usage.export.empty.title'));
}
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'));
}
}
if (periodSelect) {
periodSelect.value = state.period;
periodSelect.addEventListener('change', function () {
state.period = this.value;
loadUsage();
});
}
if (exportBtn) {
exportBtn.addEventListener('click', exportCsv);
}
window.addEventListener('crank:workspacechange', loadUsage);
if (window.whenWorkspacesReady) {
window.whenWorkspacesReady().finally(loadUsage);
} else {
loadUsage();
}
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+424
View File
@@ -0,0 +1,424 @@
function buildToolDescription() {
var snapshot = wizardCurrentVersion && wizardCurrentVersion.snapshot
? wizardCurrentVersion.snapshot
: wizardCurrentVersion;
var existing = snapshot && snapshot.tool_description ? snapshot.tool_description : {};
return {
title: textValue('tool-title') || textValue('tool-display-name') || textValue('tool-name'),
description: textValue('tool-description'),
tags: Array.isArray(existing.tags) ? existing.tags.slice() : [],
examples: Array.isArray(existing.examples) ? existing.examples.slice() : [],
};
}
function buildWizardState() {
return {
input_sample: parseStructuredText(textValue('wizard-input-sample')),
output_sample: parseStructuredText(textValue('wizard-output-sample')),
test_input: parseStructuredText(textValue('wizard-test-input')),
};
}
function collectWizardPayload() {
var name = textValue('tool-name');
if (!name) throw new Error(tKey('wizard.error.tool_name'));
var inputSchemaValue = parseStructuredText(textValue('tool-input-schema'));
var outputSchemaValue = parseStructuredText(textValue('tool-output-schema'));
var inputMappingValue = parseStructuredText(textValue('tool-input-mapping'));
var outputMappingValue = parseStructuredText(textValue('tool-output-mapping'));
return {
name: name,
display_name: textValue('tool-display-name') || name,
category: 'general',
protocol: wizardProtocol,
target: buildTarget(),
input_schema: convertJsonSchemaToCrankSchema(inputSchemaValue, []),
output_schema: convertJsonSchemaToCrankSchema(outputSchemaValue, []),
input_mapping: buildMappingSet(inputMappingValue, 'input'),
output_mapping: buildMappingSet(outputMappingValue, 'output'),
execution_config: parseExecutionConfig(textValue('tool-exec-config')),
tool_description: buildToolDescription(),
wizard_state: buildWizardState(),
};
}
async function saveOperation(stayOnPage) {
try {
await persistCurrentDraft(stayOnPage);
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('wizard.error.save'), tKey('wizard.error.save_title'));
}
}
}
async function loadOperationForEdit() {
if (!wizardWorkspaceId || !wizardEditId) return;
var detail = await window.CrankApi.getOperation(wizardWorkspaceId, wizardEditId);
wizardProtocol = detail.protocol || 'rest';
await loadWizardPanels([3]);
var draftVersion = await window.CrankApi.getOperationVersion(
wizardWorkspaceId,
wizardEditId,
detail.draft_version_ref.version
);
wizardCurrentOperation = detail;
wizardCurrentVersion = draftVersion;
prefillWizardFromEdit(detail, draftVersion);
}
function setEditModePresentation() {
var progressLabel = document.querySelector('.progress-label');
if (progressLabel) progressLabel.textContent = tKey('wizard.progress.edit');
renderSidebarBrand('edit');
}
function selectProtocol(protocol) {
wizardProtocol = protocol || 'rest';
document.querySelectorAll('.protocol-card').forEach(function(card) {
var proto = card.dataset.protocol || 'rest';
var selected = proto === wizardProtocol;
card.classList.toggle('selected', selected);
card.setAttribute('aria-checked', selected ? 'true' : 'false');
});
var s3name = document.getElementById('sidebar-step-3-name');
if (s3name) {
var step3Labels = window.CrankWizardState.step3Labels();
s3name.textContent = step3Labels[wizardProtocol] || tKey('wizard.step3.rest.label');
}
updateWizardProtocolVisibility();
}
function bindWizardLiveActions() {
bindLiveAction('wizard-upload-input-sample', tKey('wizard.busy.input_sample'), uploadInputSampleFromWizard);
bindLiveAction('wizard-upload-output-sample', tKey('wizard.busy.output_sample'), uploadOutputSampleFromWizard);
bindLiveAction('wizard-generate-draft', tKey('wizard.busy.generate'), generateDraftFromWizard);
bindLiveAction('wizard-run-test', tKey('wizard.busy.test'), runWizardTest);
bindClick('wizard-copy-test-response', copyTestResponseToOutputSample);
bindLiveAction('wizard-export-yaml', tKey('wizard.busy.export_yaml'), exportWizardYaml);
bindLiveAction('wizard-import-yaml', tKey('wizard.busy.import_yaml'), importWizardYaml);
bindLiveAction('wizard-publish-operation', tKey('wizard.busy.publish'), publishWizardOperation);
bindClick('wizard-import-yaml-file-trigger', function() {
var input = document.getElementById('wizard-import-yaml-file');
if (input) input.click();
});
var yamlInput = document.getElementById('wizard-import-yaml-file');
if (yamlInput) yamlInput.addEventListener('change', handleImportYamlFileSelection);
}
function bindClick(id, handler) {
var element = document.getElementById(id);
if (!element) return;
element.addEventListener('click', function(event) {
event.preventDefault();
handler();
});
}
function bindLiveAction(id, busyLabel, handler) {
var element = document.getElementById(id);
if (!element) return;
element.addEventListener('click', function(event) {
event.preventDefault();
runWizardLiveAction(element, busyLabel, handler);
});
}
async function runWizardLiveAction(button, busyLabel, handler) {
if (!button || button.dataset.busy === 'true') {
return;
}
var originalLabel = button.textContent;
button.dataset.busy = 'true';
button.disabled = true;
button.classList.add('is-busy');
button.textContent = busyLabel;
try {
await handler();
} catch (error) {
showWizardLiveStatus(
tKey('wizard.live.failed_title'),
error && error.message ? error.message : tKey('wizard.live.failed_body'),
true
);
if (window.CrankUi) {
window.CrankUi.error(
error && error.message ? error.message : tKey('wizard.live.failed_body'),
tKey('wizard.live.failed_title')
);
}
} finally {
button.dataset.busy = 'false';
button.disabled = false;
button.classList.remove('is-busy');
button.textContent = originalLabel;
}
}
function updateWizardProtocolVisibility() {
if (typeof window.renderEditionCapabilityHints === 'function'
&& window.CrankWizardState
&& typeof window.CrankWizardState.currentEditionCapabilities === 'function') {
window.renderEditionCapabilityHints(window.CrankWizardState.currentEditionCapabilities());
}
}
function showWizardLiveStatus(title, text, isError) {
var root = document.getElementById('wizard-live-status');
var titleEl = document.getElementById('wizard-live-status-title');
var textEl = document.getElementById('wizard-live-status-text');
if (!root || !titleEl || !textEl) return;
titleEl.textContent = title;
textEl.textContent = text;
root.hidden = false;
root.classList.toggle('is-error', !!isError);
root.classList.toggle('is-success', !isError);
}
function currentDraftVersion() {
if (wizardCurrentOperation && wizardCurrentOperation.draft_version_ref) {
return wizardCurrentOperation.draft_version_ref.version;
}
if (wizardCurrentOperation && wizardCurrentOperation.current_draft_version) {
return wizardCurrentOperation.current_draft_version;
}
if (wizardCurrentVersion && wizardCurrentVersion.version) {
return wizardCurrentVersion.version;
}
return 1;
}
function updateOperationPayload(payload) {
return {
display_name: payload.display_name,
category: payload.category,
target: payload.target,
input_schema: payload.input_schema,
output_schema: payload.output_schema,
input_mapping: payload.input_mapping,
output_mapping: payload.output_mapping,
execution_config: payload.execution_config,
tool_description: payload.tool_description,
wizard_state: payload.wizard_state,
};
}
async function refreshCurrentOperationState() {
if (!wizardWorkspaceId || !wizardEditId) return null;
var detail = await window.CrankApi.getOperation(wizardWorkspaceId, wizardEditId);
var versionNumber = detail.draft_version_ref
? detail.draft_version_ref.version
: detail.current_draft_version;
var versionDocument = await window.CrankApi.getOperationVersion(
wizardWorkspaceId,
wizardEditId,
versionNumber
);
wizardCurrentOperation = detail;
wizardCurrentVersion = versionDocument;
updateWizardProtocolVisibility();
return {
detail: detail,
version: versionDocument,
};
}
async function persistCurrentDraft(stayOnPage) {
if (!wizardWorkspaceId) {
throw new Error(tKey('wizard.error.no_workspace'));
}
var payload = collectWizardPayload();
var response;
var createdNow = false;
if (wizardMode === 'edit' && wizardEditId) {
response = await window.CrankApi.updateOperation(
wizardWorkspaceId,
wizardEditId,
updateOperationPayload(payload)
);
} else {
response = await window.CrankApi.createOperation(wizardWorkspaceId, payload);
createdNow = true;
wizardEditId = response.operation_id;
wizardMode = 'edit';
history.replaceState(
{},
'',
window.location.pathname + '?mode=edit&operationId=' + encodeURIComponent(wizardEditId)
);
setEditModePresentation();
_doGoToStep(currentStep);
var toolNameInput = document.getElementById('tool-name');
if (toolNameInput) toolNameInput.disabled = true;
}
await refreshCurrentOperationState();
if (stayOnPage) {
var saveDraftBtn = document.querySelector('.btn-save-draft');
if (saveDraftBtn) {
var label = saveDraftBtn.textContent;
saveDraftBtn.textContent = tKey('wizard.save.saved');
setTimeout(function() {
saveDraftBtn.textContent = label;
}, 1400);
}
}
showWizardLiveStatus(
createdNow ? tKey('wizard.save.created') : tKey('wizard.save.updated'),
tKey('wizard.save.body')
);
return response;
}
function safeStringify(value) {
if (value === null || value === undefined) return '';
if (typeof value === 'string') return value;
return JSON.stringify(value, null, 2);
}
function downloadTextFile(fileName, content, mimeType) {
var blob = new Blob([content], { type: mimeType || 'text/plain;charset=utf-8' });
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = fileName;
link.click();
URL.revokeObjectURL(url);
}
function setTextareaValue(id, value) {
var element = document.getElementById(id);
if (!element) return;
element.value = safeStringify(value);
}
async function exportWizardYaml() {
await persistCurrentDraft(true);
var yaml = await window.CrankApi.exportOperation(wizardWorkspaceId, wizardEditId, {
mode: 'portable',
version: currentDraftVersion(),
});
downloadTextFile((textValue('tool-name') || 'operation') + '.yaml', yaml, 'application/yaml');
showWizardLiveStatus(tKey('wizard.yaml.exported'), tKey('wizard.yaml.exported_body'));
}
async function importWizardYaml() {
if (!wizardWorkspaceId) {
throw new Error(tKey('wizard.error.no_workspace'));
}
var yamlDocument = textValue('wizard-import-yaml-text');
if (!yamlDocument) {
showWizardLiveStatus(tKey('wizard.yaml.none'), tKey('wizard.yaml.none_body'), true);
return;
}
var imported = await window.CrankApi.importOperation(wizardWorkspaceId, yamlDocument, 'upsert');
showWizardLiveStatus(tKey('wizard.yaml.imported'), tKey('wizard.yaml.imported_body'));
window.location.href = window.location.pathname + '?mode=edit&operationId=' + encodeURIComponent(imported.operation_id);
}
async function publishWizardOperation() {
await persistCurrentDraft(true);
var published = await window.CrankApi.publishOperation(
wizardWorkspaceId,
wizardEditId,
currentDraftVersion()
);
await refreshCurrentOperationState();
showWizardLiveStatus(
tKey('wizard.publish.done'),
tfKey('wizard.publish.done_body', { version: published.published_version })
);
}
function handleImportYamlFileSelection(event) {
var file = event.target.files && event.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(loadEvent) {
setValue('wizard-import-yaml-text', loadEvent.target.result || '');
showWizardLiveStatus(tKey('wizard.yaml.loaded'), tKey('wizard.yaml.loaded_body'));
};
reader.readAsText(file);
}
function describeWizardTestResult(result) {
return {
title: result.ok ? tKey('wizard.test.completed') : tKey('wizard.test.failed'),
body: result.ok ? tKey('wizard.test.completed_body') : tKey('wizard.test.failed_body'),
isError: !result.ok,
};
}
async function uploadInputSampleFromWizard() {
await persistCurrentDraft(true);
await window.CrankApi.uploadInputSample(
wizardWorkspaceId,
wizardEditId,
parseStructuredText(textValue('wizard-input-sample'))
);
showWizardLiveStatus(tKey('wizard.sample.input_saved'), tKey('wizard.sample.input_saved_body'));
}
async function uploadOutputSampleFromWizard() {
await persistCurrentDraft(true);
await window.CrankApi.uploadOutputSample(
wizardWorkspaceId,
wizardEditId,
parseStructuredText(textValue('wizard-output-sample'))
);
showWizardLiveStatus(tKey('wizard.sample.output_saved'), tKey('wizard.sample.output_saved_body'));
}
async function generateDraftFromWizard() {
await persistCurrentDraft(true);
var generated = await window.CrankApi.generateDraft(wizardWorkspaceId, wizardEditId, {});
setValue('tool-input-schema', JSON.stringify(crankSchemaToJsonSchema(generated.input_schema), null, 2));
setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(generated.output_schema), null, 2));
setValue('tool-input-mapping', mappingSetToEditorValue(generated.input_mapping, 'input', wizardProtocol));
setValue('tool-output-mapping', mappingSetToEditorValue(generated.output_mapping, 'output', wizardProtocol));
showWizardLiveStatus(tKey('wizard.sample.generated'), tKey('wizard.sample.generated_body'));
}
async function runWizardTest() {
await persistCurrentDraft(true);
var result = await window.CrankApi.runOperationTest(wizardWorkspaceId, wizardEditId, {
version: currentDraftVersion(),
input: parseStructuredText(textValue('wizard-test-input')),
});
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 : []);
var status = describeWizardTestResult(result);
showWizardLiveStatus(
status.title,
status.body,
status.isError
);
}
function copyTestResponseToOutputSample() {
if (!wizardTestResponsePreview) {
showWizardLiveStatus(tKey('wizard.test.no_response'), tKey('wizard.test.no_response_body'), true);
return;
}
setTextareaValue('wizard-output-sample', wizardTestResponsePreview);
showWizardLiveStatus(tKey('wizard.test.response_copied'), tKey('wizard.test.response_copied_body'));
}
window.CrankWizardLive = {
saveOperation: saveOperation,
loadOperationForEdit: loadOperationForEdit,
bindWizardLiveActions: bindWizardLiveActions,
updateWizardProtocolVisibility: updateWizardProtocolVisibility,
};
+406
View File
@@ -0,0 +1,406 @@
function inferSchemaKind(typeName) {
if (typeName === 'object') return 'object';
if (typeName === 'array') return 'array';
if (typeName === 'string') return 'string';
if (typeName === 'integer') return 'integer';
if (typeName === 'number') return 'number';
if (typeName === 'boolean') return 'boolean';
if (typeName === 'null') return 'null';
return 'string';
}
function convertJsonSchemaToCrankSchema(node, requiredNames) {
var schemaNode = node || {};
var typeName = Array.isArray(schemaNode.type)
? schemaNode.type.find(function(value) { return value !== 'null'; }) || 'string'
: schemaNode.type || (schemaNode.properties ? 'object' : 'string');
var nullable = Array.isArray(schemaNode.type) && schemaNode.type.indexOf('null') >= 0;
var kind = schemaNode.enum ? 'enum' : inferSchemaKind(typeName);
var schema = {
type: kind,
description: schemaNode.description || null,
required: false,
nullable: nullable,
default_value: Object.prototype.hasOwnProperty.call(schemaNode, 'default') ? schemaNode.default : null,
fields: {},
items: null,
enum_values: schemaNode.enum || [],
variants: [],
};
if (kind === 'object') {
var requiredSet = new Set(schemaNode.required || requiredNames || []);
Object.keys(schemaNode.properties || {}).forEach(function(fieldName) {
var field = convertJsonSchemaToCrankSchema(schemaNode.properties[fieldName], []);
field.required = requiredSet.has(fieldName);
schema.fields[fieldName] = field;
});
}
if (kind === 'array' && schemaNode.items) {
schema.items = convertJsonSchemaToCrankSchema(schemaNode.items, []);
}
return schema;
}
function mappingRootForProtocol(protocol) {
return '$.request.body';
}
var REQUEST_MAPPING_TARGET_PREFIXES = [
'path',
'query',
'headers',
'body',
'variables',
'grpc',
];
function normalizeInputSource(path) {
if (!path) return '$.mcp';
if (path === '$') return '$.mcp';
if (path.indexOf('$.mcp') === 0) return path;
if (path.indexOf('$.input') === 0) return '$.mcp' + path.slice('$.input'.length);
return '$.mcp.' + path.replace(/^\$\./, '');
}
function normalizeInputTarget(target) {
if (!target) return mappingRootForProtocol(wizardProtocol);
if (target.indexOf('$.request.') === 0) return target;
if (target.indexOf('request.') === 0) return '$.' + target;
var normalized = target.replace(/^\$\./, '');
var prefix = normalized.split('.')[0];
if (REQUEST_MAPPING_TARGET_PREFIXES.indexOf(prefix) >= 0) {
return '$.request.' + normalized;
}
return mappingRootForProtocol(wizardProtocol) + '.' + normalized;
}
function normalizeOutputSource(path) {
if (!path) return '$.response.body';
if (path.indexOf('$.') === 0) return path;
return '$.' + path.replace(/^\$\./, '');
}
function normalizeOutputTarget(target) {
if (!target) return '$.output';
if (target.indexOf('$.output') === 0) return target;
if (target.indexOf('output') === 0) return '$.' + target;
return '$.output.' + target.replace(/^\$\./, '');
}
function currentOperationSnapshot() {
return wizardCurrentVersion && wizardCurrentVersion.snapshot
? wizardCurrentVersion.snapshot
: wizardCurrentVersion;
}
function existingMappingRuleByTarget(mode, target) {
var snapshot = currentOperationSnapshot();
var mapping = mode === 'input'
? snapshot && snapshot.input_mapping
: snapshot && snapshot.output_mapping;
var rules = mapping && Array.isArray(mapping.rules) ? mapping.rules : [];
return rules.find(function(rule) {
return rule.target === target;
}) || null;
}
function preserveMappingRuleMetadata(rule, existingRule) {
if (!existingRule) {
rule.required = true;
return rule;
}
rule.required = existingRule.required !== undefined ? existingRule.required : true;
['default_value', 'transform', 'condition', 'notes'].forEach(function(field) {
if (existingRule[field] !== undefined && existingRule[field] !== null) {
rule[field] = existingRule[field];
}
});
return rule;
}
function buildMappingSet(rawValue, mode) {
var value = rawValue || {};
var rules = [];
Object.keys(value).forEach(function(target) {
var source = value[target];
if (mode === 'input') {
var inputTarget = normalizeInputTarget(target);
rules.push(preserveMappingRuleMetadata({
source: normalizeInputSource(source),
target: inputTarget,
}, existingMappingRuleByTarget(mode, inputTarget)));
return;
}
var outputTarget = normalizeOutputTarget(target);
rules.push(preserveMappingRuleMetadata({
source: normalizeOutputSource(source),
target: outputTarget,
}, existingMappingRuleByTarget(mode, outputTarget)));
});
return { rules: rules };
}
function parseExecutionConfig(text) {
var value = parseStructuredText(text);
var retry = value.retry || {};
var headers = value.headers && typeof value.headers === 'object' ? value.headers : {};
var selectedUpstream = currentUpstream();
if (!selectedUpstream && wizardMode !== 'edit') {
throw new Error(tKey('wizard.error.save_upstream_first'));
}
var authProfileRef = selectedUpstream && selectedUpstream.authProfileId
? selectedUpstream.authProfileId
: (value.auth && value.auth.profile ? value.auth.profile : null);
var config = {
timeout_ms: Number(value.timeout_ms || 10000),
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;
}
function currentUpstream() {
var selected = upstreams.find(function(item) { return item.id === selectedUpstreamId; });
if (selected) return selected;
var customName = textValue('new-upstream-name');
var customUrl = textValue('new-upstream-url');
if (!customUrl) return null;
var authMode = textValue('new-upstream-auth-mode') || 'none';
var authProfileId = authMode === 'existing' ? textValue('new-upstream-auth-profile') : null;
var authProfile = authProfileById(authProfileId);
return {
id: 'custom',
name: customName || 'custom-upstream',
url: customUrl,
staticHeaders: textValue('new-upstream-static-headers') || '{\n}',
authMode: authMode,
authProfileId: authProfileId || null,
authProfileName: authProfile ? authProfile.name : '',
authKind: authProfile ? authProfile.kind : null,
};
}
function joinUrl(baseUrl, path) {
if (!baseUrl) return path || '';
if (!path) return baseUrl;
return baseUrl.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
}
function parseHeaderMap(text) {
if (!text || !text.trim()) return {};
var value = parseStructuredText(text);
return value && typeof value === 'object' ? value : {};
}
function buildTarget() {
var upstream = currentUpstream();
if (!upstream || !upstream.url) {
throw new Error(tKey('wizard.error.select_upstream'));
}
var pathTemplate = textValue('endpoint-path') || '/';
var activeMethod = document.querySelector('.method-card.active');
return {
kind: 'rest',
base_url: upstream.url,
method: activeMethod ? activeMethod.dataset.method : 'POST',
path_template: pathTemplate,
static_headers: parseHeaderMap(upstream.staticHeaders),
};
}
function crankSchemaToJsonSchema(schema) {
var result = {};
var typeName = schema.type === 'enum' ? 'string' : schema.type;
result.type = schema.nullable ? [typeName, 'null'] : typeName;
if (schema.description) result.description = schema.description;
if (schema.default_value !== null && schema.default_value !== undefined) result.default = schema.default_value;
if (schema.enum_values && schema.enum_values.length) result.enum = schema.enum_values.slice();
if (schema.type === 'object') {
result.type = schema.nullable ? ['object', 'null'] : 'object';
result.properties = {};
var required = [];
Object.keys(schema.fields || {}).forEach(function(fieldName) {
var field = schema.fields[fieldName];
result.properties[fieldName] = crankSchemaToJsonSchema(field);
if (field.required) required.push(fieldName);
});
if (required.length) result.required = required;
result.additionalProperties = false;
}
if (schema.type === 'array' && schema.items) {
result.items = crankSchemaToJsonSchema(schema.items);
}
return result;
}
function exampleValueForSchema(schema) {
if (!schema) return {};
if (schema.default_value !== null && schema.default_value !== undefined) {
return schema.default_value;
}
if (schema.enum_values && schema.enum_values.length) {
return schema.enum_values[0];
}
if (schema.type === 'object') {
var object = {};
Object.keys(schema.fields || {}).forEach(function(fieldName) {
object[fieldName] = exampleValueForSchema(schema.fields[fieldName]);
});
return object;
}
if (schema.type === 'array') {
return [exampleValueForSchema(schema.items)];
}
if (schema.type === 'integer') return 1;
if (schema.type === 'number') return 1.23;
if (schema.type === 'boolean') return true;
if (schema.type === 'null') return null;
return 'string';
}
function firstToolExampleValue(toolDescription, fieldName) {
var examples = toolDescription && Array.isArray(toolDescription.examples)
? toolDescription.examples
: [];
if (!examples.length) return null;
var value = examples[0] && examples[0][fieldName];
return value === undefined ? null : value;
}
function prefillWizardSamples(snapshot) {
var state = snapshot.wizard_state || {};
var inputExample = firstToolExampleValue(snapshot.tool_description, 'input');
var inputSample = state.input_sample !== undefined && state.input_sample !== null
? state.input_sample
: inputExample !== null ? inputExample : exampleValueForSchema(snapshot.input_schema);
var outputSample = state.output_sample !== undefined && state.output_sample !== null
? state.output_sample
: exampleValueForSchema(snapshot.output_schema);
var testInput = state.test_input !== undefined && state.test_input !== null
? state.test_input
: inputSample;
setValue('wizard-input-sample', JSON.stringify(inputSample, null, 2));
setValue('wizard-test-input', JSON.stringify(testInput, null, 2));
setValue('wizard-output-sample', JSON.stringify(outputSample, null, 2));
}
function mappingSetToEditorValue(mappingSet, mode, protocol) {
var rules = mappingSet && mappingSet.rules ? mappingSet.rules : [];
var object = {};
rules.forEach(function(rule) {
if (mode === 'input') {
var key = rule.target.indexOf(mappingRootForProtocol(protocol) + '.') === 0
? rule.target.replace(mappingRootForProtocol(protocol) + '.', '')
: rule.target.replace('$.request.', '');
object[key] = rule.source.replace('$.mcp.', '$.input.');
return;
}
var outputKey = rule.target.replace('$.output.', '');
object[outputKey] = rule.source;
});
return window.jsyaml ? window.jsyaml.dump(object, { lineWidth: -1 }) : JSON.stringify(object, null, 2);
}
function executionConfigToEditorValue(config) {
var value = {
timeout_ms: config.timeout_ms,
};
if (config.retry_policy) {
value.retry = { max_attempts: config.retry_policy.max_attempts };
}
if (config.auth_profile_ref) {
value.auth = { profile: config.auth_profile_ref };
}
if (config.headers && Object.keys(config.headers).length) {
value.headers = config.headers;
}
return window.jsyaml ? window.jsyaml.dump(value, { lineWidth: -1 }) : JSON.stringify(value, null, 2);
}
function operationSnapshot(versionDocument) {
if (!versionDocument) return {};
return versionDocument.snapshot || versionDocument;
}
function prefillUpstream(baseUrl, headers, authProfileRef) {
var known = upstreams.find(function(item) {
return item.url === baseUrl && (item.authProfileId || null) === (authProfileRef || null);
});
if (known) {
pickUpstream(known.id);
return;
}
selectedUpstreamId = null;
var trigger = document.getElementById('upstream-new-trigger');
var form = document.getElementById('upstream-new-form');
if (trigger) trigger.classList.add('active');
if (form) form.hidden = false;
setValue('new-upstream-name', 'imported-upstream');
setValue('new-upstream-url', baseUrl);
setValue('new-upstream-static-headers', headers && Object.keys(headers).length ? JSON.stringify(headers, null, 2) : '{\n}');
setValue('new-upstream-auth-mode', authProfileRef ? 'existing' : 'none');
setValue('new-upstream-auth-profile', authProfileRef || '');
updateUpstreamAuthUi();
var valueEl = document.getElementById('upstream-combobox-value');
if (valueEl) {
valueEl.innerHTML = '';
valueEl.appendChild(buildUpstreamTriggerContent('imported-upstream', baseUrl));
}
}
function prefillWizardFromEdit(detail, versionDocument) {
if (!detail || !versionDocument) return;
var snapshot = operationSnapshot(versionDocument);
setEditModePresentation();
selectProtocol(snapshot.protocol || detail.protocol);
var target = snapshot.target || {};
if (target.kind === 'rest') {
prefillUpstream(target.base_url, target.static_headers, snapshot.execution_config ? snapshot.execution_config.auth_profile_ref : null);
setValue('endpoint-path', target.path_template);
document.querySelectorAll('.method-card').forEach(function(card) {
card.classList.toggle('active', card.dataset.method === target.method);
});
}
setValue('tool-name', snapshot.name || detail.name);
setValue('tool-display-name', snapshot.display_name || detail.display_name);
setValue('tool-title', snapshot.tool_description ? snapshot.tool_description.title : (snapshot.display_name || detail.display_name));
setValue('tool-description', snapshot.tool_description ? snapshot.tool_description.description : '');
setValue('tool-input-schema', JSON.stringify(crankSchemaToJsonSchema(snapshot.input_schema), null, 2));
setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(snapshot.output_schema), null, 2));
setValue('tool-input-mapping', mappingSetToEditorValue(snapshot.input_mapping, 'input', snapshot.protocol || detail.protocol));
setValue('tool-output-mapping', mappingSetToEditorValue(snapshot.output_mapping, 'output', snapshot.protocol || detail.protocol));
setValue('tool-exec-config', executionConfigToEditorValue(snapshot.execution_config || {}));
prefillWizardSamples(snapshot);
var toolNameInput = document.getElementById('tool-name');
if (toolNameInput) toolNameInput.disabled = true;
updateWizardProtocolVisibility();
}
+274
View File
@@ -0,0 +1,274 @@
(function() {
var TOTAL_STEPS = 5;
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : key;
}
function step3PanelId() {
return 'step-panel-3-rest';
}
function stepFile(step) {
return step === 3 ? 'step3-rest.html' : 'step' + step + '.html';
}
function panelIdForStep(step) {
return step === 3 ? step3PanelId() : 'step-panel-' + step;
}
function buildIconSvg(href, width, height) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', String(width));
svg.setAttribute('height', String(height));
var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', href);
svg.appendChild(use);
return svg;
}
function appendHtmlFragment(container, html) {
if (!container) return;
var template = document.createElement('template');
template.innerHTML = html;
container.appendChild(template.content.cloneNode(true));
}
function setContinueButtonContent(button, label) {
if (!button) return;
button.textContent = label + ' ';
button.appendChild(
buildIconSvg(
(window.APP_BASE || '') + 'icons/general/arrow-right.svg#icon',
13,
13
)
);
}
function renderStepCounter(counter, step, total) {
if (!counter) return;
var language = localStorage.getItem('crank_lang') || 'en';
var parts = language === 'ru'
? { prefix: 'Шаг ', suffix: ' из ' + total }
: { prefix: 'Step ', suffix: ' of ' + total };
var strong = document.createElement('strong');
counter.textContent = parts.prefix;
strong.textContent = String(step);
counter.appendChild(strong);
counter.appendChild(document.createTextNode(parts.suffix));
}
function loadStep(step, callback) {
var panelId = step === 3 ? step3PanelId() : 'step-panel-' + step;
if (document.getElementById(panelId)) {
if (callback) callback();
return;
}
fetch(stepFile(step))
.then(function(response) { return response.text(); })
.then(function(html) {
var container = document.getElementById('step-panel-container');
appendHtmlFragment(container, html);
var pane = document.getElementById(panelId);
var activeStep = window.currentStep || 1;
if (pane && step !== activeStep) {
pane.hidden = true;
}
if (typeof applyLang === 'function') applyLang();
if (callback) callback();
})
.catch(function() {
if (callback) callback();
});
}
function doGoToStep(step) {
document.querySelectorAll('.step-number[data-step]').forEach(function(element) {
var stepNumber = Number(element.getAttribute('data-step')) || 0;
element.textContent = tfKey('wizard.step_short', { step: stepNumber });
});
document.querySelectorAll('.step-pane').forEach(function(pane) { pane.hidden = true; });
var panelId = panelIdForStep(step);
var pane = document.getElementById(panelId);
if (pane) pane.hidden = false;
document.querySelectorAll('.step-item').forEach(function(item, index) {
var stepNumber = index + 1;
item.classList.remove('active', 'done', 'pending');
var indicator = item.querySelector('.step-indicator');
var status = item.querySelector('.step-status-text');
if (stepNumber < step) {
item.classList.add('done');
if (indicator) {
indicator.replaceChildren(
buildIconSvg(
(window.APP_BASE || '') + 'icons/general/check.svg#icon',
10,
10
)
);
}
if (status) status.textContent = tKey('wizard.status.completed');
} else if (stepNumber === step) {
item.classList.add('active');
if (indicator) indicator.textContent = stepNumber;
if (status) status.textContent = tKey('wizard.status.in_progress');
} else {
item.classList.add('pending');
if (indicator) indicator.textContent = stepNumber;
if (status) status.textContent = tKey('wizard.status.not_started');
}
});
var percent = Math.round((step / TOTAL_STEPS) * 100);
var fill = document.querySelector('.progress-bar-fill');
if (fill) fill.style.width = percent + '%';
var percentNode = document.querySelector('.progress-pct');
if (percentNode) percentNode.textContent = percent + '%';
var counter = pane ? pane.querySelector('[data-step-counter]') : null;
renderStepCounter(counter, step, TOTAL_STEPS);
var backButton = document.querySelector('.btn-back');
if (backButton) {
backButton.disabled = step === 1;
}
var continueButton = document.querySelector('.btn-continue');
if (continueButton) {
if (step === TOTAL_STEPS) {
var finalLabel = window.wizardMode === 'edit' ? tKey('wizard.button.save_changes') : tKey('wizard.button.create');
setContinueButtonContent(continueButton, finalLabel);
continueButton.classList.add('is-final-step');
} else {
setContinueButtonContent(continueButton, tKey('wizard.button.continue'));
continueButton.classList.remove('is-final-step');
}
}
window.currentStep = step;
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function goToStep(step) {
if (step < 1 || step > TOTAL_STEPS) return;
loadStep(step, function() { doGoToStep(step); });
}
function loadWizardPanels(steps) {
return steps.reduce(function(chain, step) {
return chain.then(function() {
return new Promise(function(resolve) {
loadStep(step, resolve);
});
});
}, Promise.resolve());
}
window.addEventListener('crank:langchange', function() {
if (window.currentStep) {
doGoToStep(window.currentStep);
}
});
function bindProtocolCards() {
document.querySelectorAll('.protocol-card').forEach(function(card) {
card.addEventListener('click', function() {
if (card.hidden || card.getAttribute('aria-disabled') === 'true') {
return;
}
document.querySelectorAll('.protocol-card').forEach(function(item) {
item.classList.remove('selected');
item.setAttribute('aria-checked', 'false');
});
card.classList.add('selected');
card.setAttribute('aria-checked', 'true');
var protocol = card.dataset.protocol
|| 'rest';
window.wizardProtocol = protocol;
var step3Name = document.getElementById('sidebar-step-3-name');
var labels = window.CrankWizardState.step3Labels();
if (step3Name) step3Name.textContent = labels[protocol] || tKey('wizard.step3.rest.label');
});
card.addEventListener('keydown', function(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
card.click();
}
});
});
}
function applyEditionProtocolVisibility(capabilities) {
var supported = capabilities && Array.isArray(capabilities.supported_protocols)
? capabilities.supported_protocols.slice()
: ['rest'];
var knownProtocols = ['rest', 'graphql', 'grpc', 'websocket', 'soap'];
document.querySelectorAll('.protocol-card').forEach(function(card) {
var protocol = card.dataset.protocol || 'rest';
var isSupported = supported.indexOf(protocol) >= 0;
card.hidden = !isSupported;
card.setAttribute('aria-disabled', isSupported ? 'false' : 'true');
if (!isSupported) {
card.classList.remove('selected');
card.setAttribute('aria-checked', 'false');
}
});
var hiddenProtocols = knownProtocols.filter(function(protocol) {
return supported.indexOf(protocol) === -1;
});
if (supported.indexOf(window.wizardProtocol) === -1) {
window.wizardProtocol = supported[0] || 'rest';
}
var selectedCard = document.querySelector('.protocol-card[data-protocol="' + window.wizardProtocol + '"]');
if (selectedCard) {
selectedCard.classList.add('selected');
selectedCard.setAttribute('aria-checked', 'true');
}
var step3Name = document.getElementById('sidebar-step-3-name');
var labels = window.CrankWizardState.step3Labels();
if (step3Name) {
step3Name.textContent = labels[window.wizardProtocol] || tKey('wizard.step3.rest.label');
}
var note = document.getElementById('wizard-edition-protocol-note');
var noteText = document.getElementById('wizard-edition-protocol-note-text');
if (note && noteText) {
if (hiddenProtocols.length) {
note.hidden = false;
noteText.textContent = tfKey('wizard.step1.community_protocol_note', {
protocols: hiddenProtocols.map(function(protocol) {
return tKey('settings.capability.' + protocol);
}).join(', ')
});
} else {
note.hidden = true;
}
}
}
window.CrankWizardShell = {
TOTAL_STEPS: TOTAL_STEPS,
step3PanelId: step3PanelId,
loadStep: loadStep,
doGoToStep: doGoToStep,
goToStep: goToStep,
loadWizardPanels: loadWizardPanels,
bindProtocolCards: bindProtocolCards,
applyEditionProtocolVisibility: applyEditionProtocolVisibility,
};
}());
+124
View File
@@ -0,0 +1,124 @@
(function() {
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
var defaults = {
currentStep: 1,
wizardProtocol: 'rest',
wizardMode: 'create',
wizardEditId: null,
wizardWorkspaceId: null,
wizardCurrentOperation: null,
wizardCurrentVersion: null,
wizardTestResponsePreview: null,
wizardProtocolCapabilities: null,
wizardEditionCapabilities: null,
wizardSecrets: [],
wizardAuthProfiles: [],
selectedUpstreamId: null,
editingUpstreamId: null,
protoParsed: null,
selectedRpcMethod: null,
};
Object.keys(defaults).forEach(function(key) {
if (typeof window[key] !== 'undefined') {
return;
}
var value = defaults[key];
if (Array.isArray(value)) {
window[key] = value.slice();
return;
}
if (value && typeof value === 'object') {
window[key] = Object.assign({}, value);
return;
}
window[key] = value;
});
function defaultProtocolCapabilities() {
return {
rest: {
supports_execution_modes: ['unary'],
supports_transport_behaviors: ['request_response'],
},
};
}
function defaultEditionCapabilities() {
return {
edition: 'community',
supported_protocols: ['rest'],
supported_security_levels: ['standard'],
machine_access_modes: ['static_agent_key'],
limits: {
max_workspaces: 1,
max_users_per_workspace: 1,
max_agents_per_workspace: 1,
},
};
}
function currentProtocolCapabilities() {
var capabilities = window.wizardProtocolCapabilities || defaultProtocolCapabilities();
return capabilities[window.wizardProtocol] || capabilities.rest;
}
function currentEditionCapabilities() {
return window.wizardEditionCapabilities || defaultEditionCapabilities();
}
async function loadProtocolCapabilities() {
if (!window.wizardWorkspaceId || !window.CrankApi || typeof window.CrankApi.getProtocolCapabilities !== 'function') {
window.wizardProtocolCapabilities = defaultProtocolCapabilities();
return window.wizardProtocolCapabilities;
}
try {
var response = await window.CrankApi.getProtocolCapabilities(window.wizardWorkspaceId);
var mapped = {};
(response.items || []).forEach(function(item) {
mapped[item.protocol] = item;
});
window.wizardProtocolCapabilities = mapped;
} catch (_error) {
window.wizardProtocolCapabilities = defaultProtocolCapabilities();
}
return window.wizardProtocolCapabilities;
}
async function loadEditionCapabilities() {
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
window.wizardEditionCapabilities = defaultEditionCapabilities();
return window.wizardEditionCapabilities;
}
try {
window.wizardEditionCapabilities = await window.CrankApi.getCapabilities();
} catch (_error) {
window.wizardEditionCapabilities = defaultEditionCapabilities();
}
return window.wizardEditionCapabilities;
}
function step3Labels() {
return {
rest: tKey('wizard.step3.rest.label'),
};
}
window.CrankWizardState = {
state: window,
defaultProtocolCapabilities: defaultProtocolCapabilities,
defaultEditionCapabilities: defaultEditionCapabilities,
currentProtocolCapabilities: currentProtocolCapabilities,
currentEditionCapabilities: currentEditionCapabilities,
loadProtocolCapabilities: loadProtocolCapabilities,
loadEditionCapabilities: loadEditionCapabilities,
step3Labels: step3Labels,
};
}());
+556
View File
@@ -0,0 +1,556 @@
/* ── Upstream selector (Step 2) ── */
var upstreams = [
{ id: 'open-meteo', name: 'Open Meteo', url: 'https://api.open-meteo.com', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null }
];
var dropdownOpen = false;
function createDivWithClass(className, text) {
var element = document.createElement('div');
element.className = className;
element.textContent = text;
return element;
}
function authProfileById(authProfileId) {
return wizardAuthProfiles.find(function(item) { return item.id === authProfileId; }) || null;
}
function syncUpstreamAuthMetadata(upstream) {
if (!upstream) return upstream;
if (!upstream.authProfileId) {
upstream.authProfileName = '';
upstream.authKind = null;
return upstream;
}
var profile = authProfileById(upstream.authProfileId);
if (profile) {
upstream.authProfileName = profile.name;
upstream.authKind = profile.kind;
}
return upstream;
}
function inferUpstreamAuth(upstream) {
syncUpstreamAuthMetadata(upstream);
if (!upstream || !upstream.authProfileId || !upstream.authKind) {
return { kind: 'none', label: tKey('wizard.step2.auth_none') };
}
if (upstream.authKind === 'bearer') {
return { kind: 'bearer', label: tKey('wizard.step2.auth_bearer') };
}
if (upstream.authKind === 'basic') {
return { kind: 'basic', label: tKey('wizard.step2.auth_basic') };
}
return { kind: 'apikey', label: tKey('wizard.step2.auth_apikey') };
}
function upstreamMeta(upstream) {
var auth = inferUpstreamAuth(upstream);
return {
kind: auth.kind,
label: auth.label,
};
}
async function loadWizardAuthResources() {
if (!wizardWorkspaceId || !window.CrankApi) {
wizardSecrets = [];
wizardAuthProfiles = [];
renderAuthProfileOptions();
return;
}
try {
var results = await Promise.all([
window.CrankApi.listSecrets(wizardWorkspaceId),
window.CrankApi.listAuthProfiles(wizardWorkspaceId),
]);
wizardSecrets = (results[0] && results[0].items) || [];
wizardAuthProfiles = (results[1] && results[1].items) || [];
} catch (_error) {
wizardSecrets = [];
wizardAuthProfiles = [];
}
upstreams.forEach(syncUpstreamAuthMetadata);
renderAuthProfileOptions();
}
function renderAuthProfileOptions() {
var profileSelect = document.getElementById('new-upstream-auth-profile');
var secretSelect = document.getElementById('new-auth-profile-secret-id');
var usernameSelect = document.getElementById('new-auth-profile-username-secret');
var passwordSelect = document.getElementById('new-auth-profile-password-secret');
var profileHint = document.getElementById('new-upstream-auth-profile-hint');
if (profileSelect) {
var previousProfile = profileSelect.value;
profileSelect.innerHTML = '';
if (!wizardAuthProfiles.length) {
var emptyProfile = document.createElement('option');
emptyProfile.value = '';
emptyProfile.textContent = tKey('wizard.step2.auth_profile_empty');
profileSelect.appendChild(emptyProfile);
if (profileHint) profileHint.textContent = tKey('wizard.step2.auth_profile_empty');
} else {
var placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = tKey('wizard.step2.auth_profile');
profileSelect.appendChild(placeholder);
wizardAuthProfiles.forEach(function(profile) {
var option = document.createElement('option');
option.value = profile.id;
option.textContent = profile.name + ' · ' + profile.kind;
profileSelect.appendChild(option);
});
if (previousProfile) {
profileSelect.value = previousProfile;
}
if (profileHint) profileHint.textContent = tKey('wizard.step2.auth_profile_hint');
}
}
[secretSelect, usernameSelect, passwordSelect].forEach(function(select) {
if (!select) return;
var previous = select.value;
select.innerHTML = '';
var placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = tKey('wizard.step2.secret_value');
select.appendChild(placeholder);
wizardSecrets.forEach(function(secret) {
var option = document.createElement('option');
option.value = secret.id;
option.textContent = secret.name + ' · ' + secret.kind;
select.appendChild(option);
});
if (previous) {
select.value = previous;
}
});
}
function updateAuthProfileCreateUi() {
var kind = textValue('new-auth-profile-kind') || 'bearer';
var headerGroup = document.getElementById('auth-profile-header-name-group');
var queryGroup = document.getElementById('auth-profile-query-param-group');
var basicRow = document.getElementById('auth-profile-basic-secret-row');
var singleSecretGroup = document.getElementById('auth-profile-single-secret-group');
if (headerGroup) {
headerGroup.hidden = !(kind === 'bearer' || kind === 'api_key_header');
}
if (queryGroup) {
queryGroup.hidden = kind !== 'api_key_query';
}
if (basicRow) {
basicRow.hidden = kind !== 'basic';
}
if (singleSecretGroup) {
singleSecretGroup.hidden = kind === 'basic';
}
}
function updateUpstreamAuthUi() {
var mode = textValue('new-upstream-auth-mode') || 'none';
var existingGroup = document.getElementById('upstream-auth-existing-group');
var createGroup = document.getElementById('upstream-auth-create-group');
if (existingGroup) existingGroup.hidden = mode !== 'existing';
if (createGroup) createGroup.hidden = mode !== 'create';
updateAuthProfileCreateUi();
}
function buildUpstreamTriggerContent(name, url) {
var fragment = document.createDocumentFragment();
fragment.appendChild(createDivWithClass('upstream-trigger-name', name));
fragment.appendChild(createDivWithClass('upstream-trigger-url', url));
return fragment;
}
function setUpstreamComboboxPlaceholder(container) {
if (!container) return;
container.innerHTML = '';
var placeholder = document.createElement('span');
placeholder.className = 'upstream-combobox-placeholder';
placeholder.textContent = tKey('wizard.step2.upstream_placeholder');
container.appendChild(placeholder);
}
function renderDropdownList(filter) {
var list = document.getElementById('upstream-dropdown-list');
if (!list) return;
var q = (filter || '').toLowerCase();
var filtered = upstreams.filter(function(u) {
return !q || u.name.toLowerCase().includes(q) || u.url.toLowerCase().includes(q);
});
if (filtered.length === 0) {
list.innerHTML = '';
var empty = document.createElement('div');
empty.className = 'upstream-dropdown-empty';
empty.textContent = tfKey('agents.drawer.ops_no_match', { query: filter || '' });
list.appendChild(empty);
return;
}
var tmpl = document.getElementById('tmpl-upstream-item');
list.innerHTML = '';
filtered.forEach(function(u) {
var sel = u.id === selectedUpstreamId;
var meta = upstreamMeta(u);
var node = tmpl.content.cloneNode(true);
var item = node.querySelector('.upstream-dropdown-item');
if (sel) item.classList.add('selected');
item.addEventListener('click', function() { pickUpstream(u.id); });
node.querySelector('.upstream-dropdown-item-name').textContent = u.name;
node.querySelector('.upstream-dropdown-item-url').textContent = u.url;
var badge = node.querySelector('.upstream-auth-badge');
badge.textContent = meta.label;
badge.className = 'upstream-auth-badge auth-' + meta.kind;
list.appendChild(node);
});
}
function openUpstreamDropdown() {
var combobox = document.getElementById('upstream-combobox');
var dropdown = document.getElementById('upstream-dropdown');
var search = document.getElementById('upstream-search');
if (!combobox || !dropdown) return;
combobox.classList.add('open');
dropdown.hidden = false;
dropdownOpen = true;
renderDropdownList('');
if (search) { search.value = ''; search.focus(); }
var form = document.getElementById('upstream-new-form');
if (form) form.hidden = true;
}
function closeUpstreamDropdown() {
var combobox = document.getElementById('upstream-combobox');
var dropdown = document.getElementById('upstream-dropdown');
if (!combobox || !dropdown) return;
combobox.classList.remove('open');
dropdown.hidden = true;
dropdownOpen = false;
}
function toggleUpstreamDropdown(e) {
e.stopPropagation();
if (dropdownOpen) { closeUpstreamDropdown(); } else { openUpstreamDropdown(); }
}
function filterUpstreams(val) {
renderDropdownList(val);
}
function pickUpstream(id) {
selectedUpstreamId = id;
editingUpstreamId = null;
closeUpstreamDropdown();
var u = upstreams.find(function(x) { return x.id === id; });
if (!u) return;
var val = document.getElementById('upstream-combobox-value');
if (val) {
var trigTmpl = document.getElementById('tmpl-upstream-trigger-value');
if (trigTmpl) {
val.innerHTML = '';
var trigNode = trigTmpl.content.cloneNode(true);
trigNode.querySelector('.upstream-trigger-name').textContent = u.name;
trigNode.querySelector('.upstream-trigger-url').textContent = u.url;
val.appendChild(trigNode);
} else {
val.innerHTML = '';
val.appendChild(buildUpstreamTriggerContent(u.name, u.url));
}
}
var preview = document.getElementById('upstream-preview');
var pName = document.getElementById('upstream-preview-name');
var pUrl = document.getElementById('upstream-preview-url');
var pBadge = document.getElementById('upstream-preview-badge');
if (preview) {
var meta = upstreamMeta(u);
pName.textContent = u.name;
pUrl.textContent = u.url;
pBadge.textContent = meta.label;
pBadge.className = 'upstream-auth-badge auth-' + meta.kind;
preview.hidden = false;
}
var form = document.getElementById('upstream-new-form');
if (form) form.hidden = true;
var trigger = document.getElementById('upstream-new-trigger');
if (trigger) trigger.classList.remove('active');
}
function startNewUpstream() {
closeUpstreamDropdown();
editingUpstreamId = null;
var trigger = document.getElementById('upstream-new-trigger');
var form = document.getElementById('upstream-new-form');
var isOpen = form && !form.hidden;
if (isOpen) {
if (form) form.hidden = true;
if (trigger) trigger.classList.remove('active');
return;
}
var val = document.getElementById('upstream-combobox-value');
if (val) setUpstreamComboboxPlaceholder(val);
var preview = document.getElementById('upstream-preview');
if (preview) preview.hidden = true;
selectedUpstreamId = null;
if (trigger) trigger.classList.add('active');
if (form) {
form.hidden = false;
setValue('new-upstream-name', '');
setValue('new-upstream-url', '');
setValue('new-upstream-static-headers', '{\n}');
setValue('new-upstream-auth-mode', 'none');
setValue('new-upstream-auth-profile', '');
setValue('new-auth-profile-name', '');
setValue('new-auth-profile-kind', 'bearer');
setValue('new-auth-profile-header-name', 'Authorization');
setValue('new-auth-profile-query-param', 'api_key');
setValue('new-auth-profile-secret-id', '');
setValue('new-auth-profile-username-secret', '');
setValue('new-auth-profile-password-secret', '');
updateUpstreamAuthUi();
var first = form.querySelector('input');
if (first) setTimeout(function() { first.focus(); }, 50);
}
}
function beginEditSelectedUpstream(e) {
if (e) e.stopPropagation();
var upstream = upstreams.find(function(item) { return item.id === selectedUpstreamId; });
if (!upstream) return;
closeUpstreamDropdown();
editingUpstreamId = upstream.id;
var trigger = document.getElementById('upstream-new-trigger');
var form = document.getElementById('upstream-new-form');
var preview = document.getElementById('upstream-preview');
if (trigger) trigger.classList.add('active');
if (preview) preview.hidden = true;
if (form) {
form.hidden = false;
setValue('new-upstream-name', upstream.name);
setValue('new-upstream-url', upstream.url);
setValue('new-upstream-static-headers', upstream.staticHeaders || '{\n}');
setValue('new-upstream-auth-profile', upstream.authProfileId || '');
setValue('new-upstream-auth-mode', upstream.authProfileId ? 'existing' : 'none');
updateUpstreamAuthUi();
}
}
function buildAuthProfileConfig(kind) {
if (kind === 'bearer') {
var bearerHeader = textValue('new-auth-profile-header-name') || 'Authorization';
var bearerSecretId = textValue('new-auth-profile-secret-id');
if (!bearerHeader) throw new Error(tKey('wizard.error.header_name_required'));
if (!bearerSecretId) throw new Error(tKey('wizard.error.secret_required'));
return {
bearer: {
header_name: bearerHeader,
secret_id: bearerSecretId,
}
};
}
if (kind === 'basic') {
var usernameSecretId = textValue('new-auth-profile-username-secret');
var passwordSecretId = textValue('new-auth-profile-password-secret');
if (!usernameSecretId || !passwordSecretId) throw new Error(tKey('wizard.error.basic_secrets_required'));
return {
basic: {
username_secret_id: usernameSecretId,
password_secret_id: passwordSecretId,
}
};
}
if (kind === 'api_key_header') {
var headerName = textValue('new-auth-profile-header-name');
var headerSecretId = textValue('new-auth-profile-secret-id');
if (!headerName) throw new Error(tKey('wizard.error.header_name_required'));
if (!headerSecretId) throw new Error(tKey('wizard.error.secret_required'));
return {
api_key_header: {
header_name: headerName,
secret_id: headerSecretId,
}
};
}
var queryParam = textValue('new-auth-profile-query-param');
var querySecretId = textValue('new-auth-profile-secret-id');
if (!queryParam) throw new Error(tKey('wizard.error.query_param_required'));
if (!querySecretId) throw new Error(tKey('wizard.error.secret_required'));
return {
api_key_query: {
param_name: queryParam,
secret_id: querySecretId,
}
};
}
async function createAuthProfileFromForm() {
var name = textValue('new-auth-profile-name');
var kind = textValue('new-auth-profile-kind') || 'bearer';
if (!name) throw new Error(tKey('wizard.error.auth_profile_name'));
var profile = await window.CrankApi.createAuthProfile(wizardWorkspaceId, {
name: name,
kind: kind,
config: buildAuthProfileConfig(kind),
});
wizardAuthProfiles.push(profile);
renderAuthProfileOptions();
if (window.CrankUi) {
window.CrankUi.success(
tKey('wizard.toast.auth_profile_created_body'),
tKey('wizard.toast.auth_profile_created_title')
);
}
return profile;
}
async function saveNewUpstream(e) {
e.stopPropagation();
try {
var nameEl = document.getElementById('new-upstream-name');
var urlEl = document.getElementById('new-upstream-url');
var name = nameEl ? nameEl.value.trim() : '';
var url = urlEl ? urlEl.value.trim() : '';
if (!name) { if (nameEl) nameEl.focus(); return; }
if (!url) { if (urlEl) urlEl.focus(); return; }
var staticHeadersEl = document.getElementById('new-upstream-static-headers');
var staticHeaders = staticHeadersEl ? staticHeadersEl.value : '{\n}';
parseHeaderMap(staticHeaders);
var authMode = textValue('new-upstream-auth-mode') || 'none';
var authProfileId = null;
var authProfileName = '';
var authKind = null;
if (authMode === 'existing') {
authProfileId = textValue('new-upstream-auth-profile');
if (!authProfileId) throw new Error(tKey('wizard.error.auth_profile_required'));
var existingProfile = authProfileById(authProfileId);
authProfileName = existingProfile ? existingProfile.name : '';
authKind = existingProfile ? existingProfile.kind : null;
} else if (authMode === 'create') {
var createdProfile = await createAuthProfileFromForm();
authProfileId = createdProfile.id;
authProfileName = createdProfile.name;
authKind = createdProfile.kind;
setValue('new-upstream-auth-profile', createdProfile.id);
setValue('new-upstream-auth-mode', 'existing');
}
var nextRecord = {
id: editingUpstreamId || ('custom-' + Date.now()),
name: name,
url: url,
staticHeaders: staticHeaders,
authProfileId: authProfileId,
authProfileName: authProfileName,
authKind: authKind,
};
if (editingUpstreamId) {
upstreams = upstreams.map(function(item) {
return item.id === editingUpstreamId ? nextRecord : item;
});
} else {
upstreams.push(nextRecord);
}
if (nameEl) nameEl.value = '';
if (urlEl) urlEl.value = '';
if (staticHeadersEl) staticHeadersEl.value = '{\n}';
var form = document.getElementById('upstream-new-form');
if (form) form.hidden = true;
editingUpstreamId = null;
pickUpstream(nextRecord.id);
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || tKey('wizard.toast.auth_profile_error_title'),
tKey('wizard.toast.auth_profile_error_title')
);
}
}
}
function cancelNewUpstream(e) {
e.stopPropagation();
var form = document.getElementById('upstream-new-form');
if (form) form.hidden = true;
var trigger = document.getElementById('upstream-new-trigger');
if (trigger) trigger.classList.remove('active');
editingUpstreamId = null;
}
function openQuickSecretModal(e) {
if (e) e.preventDefault();
var modal = document.getElementById('quick-secret-modal');
if (!modal) return;
setValue('quick-secret-name', '');
setValue('quick-secret-value', '');
setValue('quick-secret-kind', 'token');
modal.classList.add('open');
setTimeout(function() {
var input = document.getElementById('quick-secret-name');
if (input) input.focus();
}, 30);
}
function closeQuickSecretModal() {
var modal = document.getElementById('quick-secret-modal');
if (modal) modal.classList.remove('open');
}
async function submitQuickSecret() {
var name = textValue('quick-secret-name');
var value = textValue('quick-secret-value');
var kind = textValue('quick-secret-kind') || 'token';
if (!name) throw new Error(tKey('wizard.error.secret_name_required'));
if (!value) throw new Error(tKey('wizard.error.secret_value_required'));
var secret = await window.CrankApi.createSecret(wizardWorkspaceId, {
name: name,
kind: kind,
value: value,
});
wizardSecrets.push(secret);
renderAuthProfileOptions();
var primarySelect = document.getElementById('new-auth-profile-secret-id');
var usernameSelect = document.getElementById('new-auth-profile-username-secret');
var passwordSelect = document.getElementById('new-auth-profile-password-secret');
if (primarySelect && primarySelect.offsetParent !== null) {
primarySelect.value = secret.id;
} else if (usernameSelect && !usernameSelect.value) {
usernameSelect.value = secret.id;
} else if (passwordSelect) {
passwordSelect.value = secret.id;
}
closeQuickSecretModal();
if (window.CrankUi) {
window.CrankUi.success(
tKey('wizard.toast.quick_secret_created_body'),
tKey('wizard.toast.quick_secret_created_title')
);
}
}
+286
View File
@@ -0,0 +1,286 @@
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : key;
}
var TOTAL_STEPS = window.CrankWizardShell.TOTAL_STEPS;
var loadProtocolCapabilities = window.CrankWizardState.loadProtocolCapabilities;
var loadEditionCapabilities = window.CrankWizardState.loadEditionCapabilities;
var currentProtocolCapabilities = window.CrankWizardState.currentProtocolCapabilities;
var loadStep = window.CrankWizardShell.loadStep;
var goToStep = window.CrankWizardShell.goToStep;
var _doGoToStep = window.CrankWizardShell.doGoToStep;
var loadWizardPanels = window.CrankWizardShell.loadWizardPanels;
var bindProtocolCards = window.CrankWizardShell.bindProtocolCards;
var applyEditionProtocolVisibility = window.CrankWizardShell.applyEditionProtocolVisibility;
var step3PanelId = window.CrankWizardShell.step3PanelId;
var saveOperation = window.CrankWizardLive.saveOperation;
var loadOperationForEdit = window.CrankWizardLive.loadOperationForEdit;
var bindWizardLiveActions = window.CrankWizardLive.bindWizardLiveActions;
var updateWizardProtocolVisibility = window.CrankWizardLive.updateWizardProtocolVisibility;
var currentStep = window.currentStep;
var wizardProtocol = window.wizardProtocol;
var wizardMode = window.wizardMode;
var wizardEditId = window.wizardEditId;
var wizardWorkspaceId = window.wizardWorkspaceId;
var wizardCurrentOperation = window.wizardCurrentOperation;
var wizardCurrentVersion = window.wizardCurrentVersion;
var wizardProtoUpload = window.wizardProtoUpload;
var wizardDescriptorSetUpload = window.wizardDescriptorSetUpload;
var wizardSoapWsdlUpload = window.wizardSoapWsdlUpload;
var wizardSoapXsdUpload = window.wizardSoapXsdUpload;
var wizardTestResponsePreview = window.wizardTestResponsePreview;
var wizardProtocolCapabilities = window.wizardProtocolCapabilities;
var wizardSecrets = window.wizardSecrets;
var wizardAuthProfiles = window.wizardAuthProfiles;
var selectedUpstreamId = window.selectedUpstreamId;
var editingUpstreamId = window.editingUpstreamId;
var protoParsed = window.protoParsed;
var selectedRpcMethod = window.selectedRpcMethod;
async function initWizardPage() {
renderSidebarBrand('create');
document.querySelector('.btn-continue').addEventListener('click', function() {
if (currentStep < TOTAL_STEPS) {
goToStep(currentStep + 1);
} else {
saveOperation();
}
});
document.querySelector('.btn-back').addEventListener('click', function() {
if (!this.disabled && currentStep > 1) goToStep(currentStep - 1);
});
document.querySelectorAll('.step-item').forEach(function(item, i) {
item.addEventListener('click', function() { goToStep(i + 1); });
});
var backToCatalog = document.getElementById('back-to-catalog');
if (backToCatalog) {
backToCatalog.addEventListener('click', function() {
window.location.href = (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) || '/';
});
}
var saveDraftBtn = document.querySelector('.btn-save-draft');
if (saveDraftBtn) {
saveDraftBtn.addEventListener('click', function() {
saveOperation(true);
});
}
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
wizardWorkspaceId = workspace ? workspace.id : null;
window.wizardWorkspaceId = wizardWorkspaceId;
var editionCapabilities = await loadEditionCapabilities();
await loadProtocolCapabilities();
await loadWizardPanels([1, 2, 3, 4, 5]);
if (window.CrankOverlay && typeof window.CrankOverlay.render === 'function') {
await window.CrankOverlay.render(document, {
workspace: workspace,
capabilities: editionCapabilities,
locale: localStorage.getItem('crank_lang') || 'en',
});
}
await loadWizardAuthResources();
bindProtocolCards();
applyEditionProtocolVisibility(editionCapabilities);
bindWizardLiveActions();
updateUpstreamAuthUi();
renderEditionCapabilityHints(editionCapabilities);
var quickSecretModal = document.getElementById('quick-secret-modal');
var quickSecretClose = document.getElementById('quick-secret-close-btn');
var quickSecretCancel = document.getElementById('quick-secret-cancel-btn');
var quickSecretSubmit = document.getElementById('quick-secret-submit-btn');
if (quickSecretClose) quickSecretClose.addEventListener('click', closeQuickSecretModal);
if (quickSecretCancel) quickSecretCancel.addEventListener('click', closeQuickSecretModal);
if (quickSecretModal) {
quickSecretModal.addEventListener('click', function(event) {
if (event.target === quickSecretModal) closeQuickSecretModal();
});
}
if (quickSecretSubmit) {
quickSecretSubmit.addEventListener('click', async function() {
var button = quickSecretSubmit;
var original = button.textContent;
button.disabled = true;
button.textContent = tKey('wizard.step2.quick_secret_submit');
try {
await submitQuickSecret();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || tKey('wizard.toast.quick_secret_error_title'),
tKey('wizard.toast.quick_secret_error_title')
);
}
} finally {
button.disabled = false;
button.textContent = original;
}
});
}
var params = new URLSearchParams(window.location.search);
if (params.get('mode') === 'edit' && params.get('operationId')) {
wizardMode = 'edit';
wizardEditId = params.get('operationId');
window.wizardMode = wizardMode;
window.wizardEditId = wizardEditId;
document.title = 'Crank — ' + tKey('wizard.progress.edit');
await loadOperationForEdit();
}
updateWizardProtocolVisibility();
_doGoToStep(1);
}
function renderEditionCapabilityHints(capabilities) {
var securityNote = document.getElementById('wizard-security-level-note');
var securityText = securityNote ? securityNote.querySelector('.info-callout-text') : null;
if (!securityText) return;
var levels = capabilities && Array.isArray(capabilities.supported_security_levels)
? capabilities.supported_security_levels
: ['standard'];
securityNote.hidden = !(levels.length === 1 && levels[0] === 'standard');
if (!securityNote.hidden) {
securityText.textContent = tKey('wizard.step5.community_security_note');
}
}
window.renderEditionCapabilityHints = renderEditionCapabilityHints;
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') {
window.CrankDiagnostics.bootstrap('wizard', initWizardPage);
} else {
document.addEventListener('DOMContentLoaded', function() {
void initWizardPage();
});
}
/* ── HTTP method picker (Step 4 REST) ── */
var METHOD_CALLOUTS = {
GET: null,
DELETE: null,
POST: { icon: 'info', text: { en: { title: 'POST selected — body encoding required.', body: 'In step 4 you will define the JSON schema for the body payload. Crank will automatically serialize MCP tool arguments into the format you specify.' }, ru: { title: 'Выбран POST — требуется кодирование тела запроса.', body: 'На шаге 4 вы зададите JSON-схему для body payload. Crank автоматически сериализует аргументы MCP-инструмента в выбранный формат.' } } },
PUT: { icon: 'info', text: { en: { title: 'PUT selected — full-resource replacement.', body: 'The request body must contain a complete resource representation. Define the body schema in step 4.' }, ru: { title: 'Выбран PUT — полная замена ресурса.', body: 'Request body должен содержать полное представление ресурса. Задайте body schema на шаге 4.' } } },
PATCH: { icon: 'info', text: { en: { title: 'PATCH selected — partial update.', body: 'Only include the fields you want to modify in the body schema defined in step 4.' }, ru: { title: 'Выбран PATCH — частичное обновление.', body: 'В body schema на шаге 4 включайте только те поля, которые хотите изменить.' } } },
};
function renderSidebarBrand(mode) {
var sidebarBrand = document.querySelector('.step-sidebar-brand');
if (!sidebarBrand) return;
var language = localStorage.getItem('crank_lang') || 'en';
var parts;
if (mode === 'edit') {
parts = language === 'ru'
? { prefix: 'Редактировать ', suffix: 'операцию' }
: { prefix: 'Edit ', suffix: 'operation' };
} else {
parts = language === 'ru'
? { prefix: 'Создать ', suffix: 'операцию' }
: { prefix: 'Create ', suffix: 'operation' };
}
sidebarBrand.dataset.wizardBrand = mode;
sidebarBrand.textContent = parts.prefix;
var accent = document.createElement('span');
accent.textContent = parts.suffix;
sidebarBrand.appendChild(accent);
}
window.addEventListener('crank:langchange', function() {
renderSidebarBrand(wizardMode === 'edit' ? 'edit' : 'create');
});
function selectMethod(btn) {
document.querySelectorAll('.method-card').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
var method = btn.dataset.method;
var callout = document.getElementById('method-callout-rest');
if (!callout) return;
var info = METHOD_CALLOUTS[method];
if (info) {
var lang = localStorage.getItem('crank_lang') || 'en';
var text = typeof info.text === 'string' ? info.text : (info.text[lang] || info.text.en);
callout.innerHTML = '';
var icon = buildIconSvg(
(window.APP_BASE || '') + 'icons/general/info-circle.svg#icon',
15,
15
);
icon.style.flexShrink = '0';
icon.style.color = 'var(--accent)';
callout.appendChild(icon);
var content = document.createElement('span');
var title = document.createElement('strong');
title.textContent = text.title;
content.appendChild(title);
content.appendChild(document.createTextNode(' ' + text.body));
callout.appendChild(content);
callout.hidden = false;
} else {
callout.replaceChildren();
callout.hidden = true;
}
}
/* ── GraphQL operation type picker (Step 4 GraphQL) ── */
function selectGqlType(btn) {
document.querySelectorAll('.gql-type-card').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
// Update query editor placeholder if it's still the default
var editor = document.getElementById('gql-query-editor');
if (!editor) return;
var type = btn.dataset.gqlType;
var defaults = {
query: 'query GetContact($id: ID!) {\n contact(id: $id) {\n id\n firstName\n lastName\n email\n company {\n id\n name\n }\n createdAt\n }\n}',
mutation: 'mutation CreateContact(\n $firstName: String!\n $lastName: String!\n $email: String!\n $company: String\n) {\n createContact(input: {\n firstName: $firstName\n lastName: $lastName\n email: $email\n company: $company\n }) {\n id\n firstName\n lastName\n createdAt\n }\n}'
};
if (defaults[type]) editor.value = defaults[type];
}
function escapeHtml(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// Close dropdown on outside click
document.addEventListener('click', function() {
if (dropdownOpen) closeUpstreamDropdown();
});
function textValue(id) {
var element = document.getElementById(id);
return element ? element.value.trim() : '';
}
function setValue(id, value) {
var element = document.getElementById(id);
if (element) element.value = value || '';
}
function parseStructuredText(text) {
if (!text.trim()) return {};
try {
return JSON.parse(text);
} catch (_error) {
if (!window.jsyaml) throw new Error(tKey('wizard.error.parser_unavailable'));
return window.jsyaml.load(text) || {};
}
}
+730
View File
@@ -0,0 +1,730 @@
var workspaceFormState = {
selectedColor: '#0d9488',
slugManual: false,
isCreateMode: false,
formDirty: false,
workspaceId: null,
workspaceRecord: null,
memberships: [],
invitations: [],
currentUserId: null,
};
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : key;
}
function normalizeSlug(value) {
return value
.toLowerCase()
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
function uiRoleToApi(value) {
if (value === 'developer') {
return 'operator';
}
return value;
}
function roleLabel(value) {
if (value === 'operator') return tKey('workspace_setup.role.operator');
if (value === 'owner') return tKey('workspace_setup.role.owner');
if (value === 'admin') return tKey('workspace_setup.role.admin');
if (value === 'viewer') return tKey('workspace_setup.role.viewer');
return value.charAt(0).toUpperCase() + value.slice(1);
}
function roleClass(value) {
if (value === 'operator') {
return 'role-developer';
}
return 'role-' + value;
}
function setFormDirty(value) {
workspaceFormState.formDirty = value;
}
function formElements() {
return {
name: document.getElementById('ws-name'),
slug: document.getElementById('ws-slug'),
desc: document.getElementById('ws-desc'),
submit: document.getElementById('submit-btn'),
avatar: document.getElementById('ws-avatar-preview'),
};
}
function createElement(tag, className, text) {
var node = document.createElement(tag);
if (className) {
node.className = className;
}
if (text !== undefined && text !== null) {
node.textContent = text;
}
return node;
}
function buildIconSvg(href, width, height, viewBox, attrs) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', String(width));
svg.setAttribute('height', String(height));
if (viewBox) {
svg.setAttribute('viewBox', viewBox);
}
if (attrs) {
Object.keys(attrs).forEach(function(key) {
svg.setAttribute(key, attrs[key]);
});
}
var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', href);
svg.appendChild(use);
return svg;
}
function workspacePayload() {
var elements = formElements();
return {
slug: elements.slug.value.trim(),
display_name: elements.name.value.trim(),
settings: {
description: elements.desc.value.trim(),
color: workspaceFormState.selectedColor,
},
};
}
function applyWorkspaceRecord(record) {
var workspace = record.workspace;
var settings = workspace.settings || {};
var elements = formElements();
workspaceFormState.workspaceId = workspace.id;
workspaceFormState.workspaceRecord = record;
workspaceFormState.selectedColor = settings.color || '#0d9488';
workspaceFormState.slugManual = true;
elements.name.value = workspace.display_name || '';
elements.slug.value = workspace.slug || '';
elements.desc.value = settings.description || '';
elements.avatar.textContent = (workspace.display_name || workspace.slug || '?').charAt(0).toUpperCase();
elements.avatar.style.background = workspaceFormState.selectedColor;
document.querySelectorAll('.ws-color-swatch').forEach(function (swatch) {
swatch.classList.toggle('active', swatch.dataset.color === workspaceFormState.selectedColor);
});
setFormDirty(false);
}
function addMemberRow(container, membership) {
var row = document.createElement('div');
row.className = 'member-row';
var isCurrentUser = membership.user.id === workspaceFormState.currentUserId;
var canManage = membership.role !== 'owner';
var avatar = createElement(
'div',
'member-avatar',
membership.user.display_name.slice(0, 2).toUpperCase()
);
avatar.style.background =
'linear-gradient(135deg,' + workspaceFormState.selectedColor + ',#6366f1)';
row.appendChild(avatar);
var info = createElement('div', 'member-info');
var name = createElement('div', 'member-name', membership.user.display_name);
if (isCurrentUser) {
name.appendChild(document.createTextNode(' '));
var currentUserLabel = createElement('span', null, '(you)');
currentUserLabel.style.fontSize = '11px';
currentUserLabel.style.fontWeight = '400';
currentUserLabel.style.color = 'var(--text-muted)';
name.appendChild(currentUserLabel);
}
info.appendChild(name);
info.appendChild(createElement('div', 'member-email', membership.user.email));
row.appendChild(info);
row.appendChild(createElement('div', 'member-last-active', '—'));
if (canManage) {
var select = createElement('select', 'member-role-select');
select.dataset.previous = membership.role;
[
['owner', tKey('workspace_setup.role.owner')],
['admin', tKey('workspace_setup.role.admin')],
['operator', tKey('workspace_setup.role.operator')],
['viewer', tKey('workspace_setup.role.viewer')]
].forEach(function(optionData) {
var option = document.createElement('option');
option.value = optionData[0];
option.textContent = optionData[1];
option.selected = membership.role === optionData[0];
select.appendChild(option);
});
select.addEventListener('change', function() {
updateRole(select, membership.user.id);
});
row.appendChild(select);
var removeButton = createElement('button', 'member-remove-btn');
removeButton.type = 'button';
removeButton.title = tKey('workspace_setup.members.remove');
removeButton.appendChild(
buildIconSvg(
'',
12,
12,
'0 0 16 16',
{
fill: 'none',
stroke: 'currentColor',
'stroke-width': '1.8',
'stroke-linecap': 'round',
'stroke-linejoin': 'round'
}
)
);
var removeSvg = removeButton.querySelector('svg');
var pathA = document.createElementNS('http://www.w3.org/2000/svg', 'path');
pathA.setAttribute('d', 'M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4');
var pathB = document.createElementNS('http://www.w3.org/2000/svg', 'path');
pathB.setAttribute('d', 'M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9');
removeSvg.appendChild(pathA);
removeSvg.appendChild(pathB);
removeButton.addEventListener('click', function() {
removeMember(membership.user.id, membership.user.display_name);
});
row.appendChild(removeButton);
} else {
row.appendChild(
createElement(
'span',
'member-role-badge ' + roleClass(membership.role),
roleLabel(membership.role)
)
);
}
container.appendChild(row);
}
function renderMembers() {
var container = document.getElementById('members-list');
var label = document.getElementById('members-count-label');
if (!container || !label) {
return;
}
container.innerHTML = '';
workspaceFormState.memberships.forEach(function (membership) {
addMemberRow(container, membership);
});
label.textContent = window.tPlural(
'workspace_setup.member_count',
workspaceFormState.memberships.length,
{ count: workspaceFormState.memberships.length }
);
}
function renderInvitations() {
var pending = document.getElementById('pending-invites');
var count = document.getElementById('pending-count');
if (!pending || !count) {
return;
}
var items = workspaceFormState.invitations.filter(function (record) {
return record.invitation.status === 'pending';
});
count.textContent = String(items.length);
if (!items.length) {
pending.hidden = true;
return;
}
pending.hidden = false;
pending.innerHTML = '';
var heading = createElement('div', null, tKey('workspace_setup.members.pending') + ' ');
heading.style.fontSize = '11px';
heading.style.fontWeight = '600';
heading.style.textTransform = 'uppercase';
heading.style.letterSpacing = '0.06em';
heading.style.color = 'var(--text-muted)';
heading.style.marginBottom = '10px';
var countPill = createElement('span', null, String(items.length));
countPill.id = 'pending-count';
countPill.style.background = 'rgba(139,148,158,0.15)';
countPill.style.borderRadius = '10px';
countPill.style.padding = '1px 7px';
countPill.style.fontSize = '10px';
heading.appendChild(countPill);
pending.appendChild(heading);
items.forEach(function (record) {
var invitation = record.invitation;
var row = document.createElement('div');
row.className = 'member-row';
var inviteAvatar = createElement('div', 'member-avatar', '?');
inviteAvatar.style.background = 'var(--bg-canvas)';
inviteAvatar.style.border = '1.5px dashed var(--border)';
inviteAvatar.style.color = 'var(--text-muted)';
inviteAvatar.style.fontSize = '16px';
row.appendChild(inviteAvatar);
var inviteInfo = createElement('div', 'member-info');
inviteInfo.appendChild(createElement('div', 'member-name', invitation.email));
inviteInfo.appendChild(
createElement(
'div',
'member-email',
tfKey('workspace_setup.members.invited_pending', { date: invitation.created_at.slice(0, 10) })
)
);
row.appendChild(inviteInfo);
row.appendChild(createElement('div', 'member-last-active', '—'));
var roleBadge = createElement('span', 'member-role-badge', roleLabel(invitation.role));
roleBadge.style.background = 'rgba(139,148,158,0.12)';
roleBadge.style.color = 'var(--text-muted)';
roleBadge.style.border = '1px solid var(--border)';
row.appendChild(roleBadge);
var revokeButton = createElement('button', 'member-remove-btn');
revokeButton.type = 'button';
revokeButton.title = tKey('workspace_setup.members.revoke');
var revokeSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
revokeSvg.setAttribute('width', '12');
revokeSvg.setAttribute('height', '12');
revokeSvg.setAttribute('viewBox', '0 0 16 16');
revokeSvg.setAttribute('fill', 'none');
revokeSvg.setAttribute('stroke', 'currentColor');
revokeSvg.setAttribute('stroke-width', '1.8');
revokeSvg.setAttribute('stroke-linecap', 'round');
var lineA = document.createElementNS('http://www.w3.org/2000/svg', 'line');
lineA.setAttribute('x1', '2');
lineA.setAttribute('y1', '2');
lineA.setAttribute('x2', '14');
lineA.setAttribute('y2', '14');
var lineB = document.createElementNS('http://www.w3.org/2000/svg', 'line');
lineB.setAttribute('x1', '14');
lineB.setAttribute('y1', '2');
lineB.setAttribute('x2', '2');
lineB.setAttribute('y2', '14');
revokeSvg.appendChild(lineA);
revokeSvg.appendChild(lineB);
revokeButton.appendChild(revokeSvg);
revokeButton.addEventListener('click', function() {
revokeInviteById(invitation.id);
});
row.appendChild(revokeButton);
pending.appendChild(row);
});
}
async function loadWorkspaceAccessData() {
if (workspaceFormState.isCreateMode || !window.CrankApi || !workspaceFormState.workspaceId) {
return;
}
if (window.CrankAuth && typeof window.CrankAuth.fetchSession === 'function') {
try {
var session = await window.CrankAuth.fetchSession(false);
workspaceFormState.currentUserId = session && session.user ? session.user.id : null;
} catch (_error) {}
}
var membershipsResponse = await window.CrankApi.listMemberships(workspaceFormState.workspaceId);
var invitationsResponse = await window.CrankApi.listInvitations(workspaceFormState.workspaceId);
workspaceFormState.memberships = membershipsResponse && membershipsResponse.items ? membershipsResponse.items : [];
workspaceFormState.invitations = invitationsResponse && invitationsResponse.items ? invitationsResponse.items : [];
renderMembers();
renderInvitations();
}
function updatePageMode() {
var params = new URLSearchParams(window.location.search);
workspaceFormState.isCreateMode = params.get('mode') === 'create';
var submit = document.getElementById('submit-btn');
if (workspaceFormState.isCreateMode) {
document.title = 'Crank — ' + tKey('workspace_setup.create.title');
document.getElementById('page-title').textContent = tKey('workspace_setup.create.title');
document.getElementById('page-subtitle').textContent = tKey('workspace_setup.create.subtitle');
document.getElementById('section-invite').hidden = false;
document.getElementById('footer-note').hidden = false;
submit.textContent = tKey('workspace_setup.actions.create');
submit.disabled = true;
submit.classList.add('is-disabled');
} else {
document.getElementById('section-members').hidden = false;
document.getElementById('section-danger').hidden = false;
submit.textContent = tKey('workspace_setup.actions.save');
submit.classList.remove('is-disabled');
}
}
function onWsNameInput(value) {
var elements = formElements();
var normalized = value.trim();
elements.avatar.textContent = normalized ? normalized.charAt(0).toUpperCase() : '?';
if (!workspaceFormState.slugManual) {
elements.slug.value = normalizeSlug(value);
}
if (workspaceFormState.isCreateMode) {
validateCreateForm();
}
setFormDirty(true);
}
function onWsSlugInput(value) {
formElements().slug.value = normalizeSlug(value);
workspaceFormState.slugManual = true;
if (workspaceFormState.isCreateMode) {
validateCreateForm();
}
setFormDirty(true);
}
function pickColor(element) {
workspaceFormState.selectedColor = element.dataset.color;
document.querySelectorAll('.ws-color-swatch').forEach(function (swatch) {
swatch.classList.remove('active');
});
element.classList.add('active');
formElements().avatar.style.background = workspaceFormState.selectedColor;
setFormDirty(true);
}
function validateCreateForm() {
var elements = formElements();
var valid = Boolean(elements.name.value.trim() && elements.slug.value.trim());
elements.submit.disabled = !valid;
elements.submit.classList.toggle('is-disabled', !valid);
}
function inviteRows() {
return Array.from(document.querySelectorAll('#invite-rows .invite-row')).map(function (row) {
return {
email: row.querySelector('input').value.trim(),
role: uiRoleToApi(row.querySelector('select').value),
};
}).filter(function (row) {
return row.email;
});
}
async function submitForm() {
var payload = workspacePayload();
if (!payload.display_name || !payload.slug || !window.CrankApi) {
return;
}
var submit = document.getElementById('submit-btn');
var originalLabel = submit.textContent;
submit.disabled = true;
submit.textContent = workspaceFormState.isCreateMode ? tKey('workspace_setup.saving_create') : tKey('workspace_setup.saving_update');
try {
var response;
if (workspaceFormState.isCreateMode) {
response = await window.CrankApi.createWorkspace(payload);
} else {
response = await window.CrankApi.updateWorkspace(workspaceFormState.workspaceId, payload);
}
var record = response && response.workspace ? response : { workspace: response.workspace || response };
var workspace = record.workspace;
if (workspaceFormState.isCreateMode) {
var invites = inviteRows();
for (var index = 0; index < invites.length; index += 1) {
await window.CrankApi.createInvitation(workspace.id, {
email: invites[index].email,
role: invites[index].role,
});
}
}
var workspaces = await window.refreshWorkspaces();
var mapped = workspaces.find(function (item) { return item.id === workspace.id; }) || {
id: workspace.id,
slug: workspace.slug,
name: workspace.display_name,
role: tKey('workspace_setup.role.owner'),
letter: (workspace.display_name || workspace.slug).charAt(0).toUpperCase(),
color: payload.settings.color || '#0d9488',
status: workspace.status,
settings: payload.settings,
};
await window.setCurrentWorkspace(mapped);
workspaceFormState.workspaceId = workspace.id;
workspaceFormState.workspaceRecord = { workspace: workspace };
setFormDirty(false);
if (workspaceFormState.isCreateMode) {
window.location.href = (window.CrankRoutes && window.CrankRoutes.workspaceSetup) || '/workspace-setup';
return;
}
applyWorkspaceRecord({ workspace: workspace });
await loadWorkspaceAccessData();
submit.textContent = tKey('workspace_setup.saved');
setTimeout(function () {
submit.textContent = originalLabel;
submit.disabled = false;
}, 1400);
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace_setup.save_error'), tKey('workspace_setup.save_error_title'));
}
submit.disabled = false;
submit.textContent = originalLabel;
}
}
function addInviteRow() {
var rows = document.getElementById('invite-rows');
var node = document.createElement('div');
node.className = 'invite-row';
var emailInput = createElement('input', 'form-input');
emailInput.type = 'email';
emailInput.placeholder = 'colleague@company.com';
emailInput.autocomplete = 'off';
emailInput.style.flex = '1';
emailInput.style.marginBottom = '0';
node.appendChild(emailInput);
var roleSelect = createElement('select', 'form-input');
roleSelect.style.width = '130px';
roleSelect.style.marginBottom = '0';
[
['admin', tKey('workspace_setup.role.admin')],
['developer', tKey('workspace_setup.role.operator')],
['viewer', tKey('workspace_setup.role.viewer')]
].forEach(function(entry) {
var option = document.createElement('option');
option.value = entry[0];
option.textContent = entry[1];
option.selected = entry[0] === 'developer';
roleSelect.appendChild(option);
});
node.appendChild(roleSelect);
var removeButton = createElement('button', 'invite-row-remove');
removeButton.type = 'button';
removeButton.title = tKey('workspace_setup.members.remove');
removeButton.appendChild(
buildIconSvg(
(window.APP_BASE || '') + 'icons/general/close.svg#icon',
12,
12
)
);
removeButton.addEventListener('click', function() {
removeInviteRow(removeButton);
});
node.appendChild(removeButton);
rows.appendChild(node);
}
function removeInviteRow(button) {
button.closest('.invite-row').remove();
}
function toggleInviteForm() {
var form = document.getElementById('invite-form');
var success = document.getElementById('invite-success');
if (!form) {
return;
}
if (form.hidden) {
form.hidden = false;
if (success) {
success.hidden = true;
}
document.getElementById('invite-email').focus();
} else {
form.hidden = true;
}
}
async function sendInvite() {
if (!window.CrankApi || !workspaceFormState.workspaceId) {
return;
}
var email = document.getElementById('invite-email').value.trim();
var role = uiRoleToApi(document.getElementById('invite-role').value);
if (!email || email.indexOf('@') === -1) {
return;
}
try {
await window.CrankApi.createInvitation(workspaceFormState.workspaceId, { email: email, role: role });
document.getElementById('invite-email').value = '';
document.getElementById('invite-success').hidden = false;
await loadWorkspaceAccessData();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace_setup.invite_error'), tKey('workspace_setup.invite_error_title'));
}
}
}
async function updateRole(select, userId) {
if (!workspaceFormState.workspaceId || !window.CrankApi) {
return;
}
var previous = select.dataset.previous || select.value;
select.disabled = true;
try {
var response = await window.CrankApi.updateMembership(workspaceFormState.workspaceId, userId, {
role: select.value,
});
workspaceFormState.memberships = response && response.items ? response.items : workspaceFormState.memberships;
renderMembers();
} catch (error) {
select.value = previous;
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace_setup.role_error'), tKey('workspace_setup.role_error_title'));
}
} finally {
select.disabled = false;
}
}
async function removeMember(userId, name) {
if (!workspaceFormState.workspaceId || !window.CrankApi) {
return;
}
if (!confirm(tfKey('workspace_setup.remove_confirm', { name: name }))) {
return;
}
try {
await window.CrankApi.deleteMembership(workspaceFormState.workspaceId, userId);
await loadWorkspaceAccessData();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace_setup.remove_error'), tKey('workspace_setup.remove_error_title'));
}
}
}
async function revokeInviteById(invitationId) {
if (!window.CrankApi || !workspaceFormState.workspaceId) {
return;
}
try {
await window.CrankApi.deleteInvitation(workspaceFormState.workspaceId, invitationId);
await loadWorkspaceAccessData();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace_setup.revoke_error'), tKey('workspace_setup.revoke_error_title'));
}
}
}
function revokeInvite(button) {
var row = button.closest('.member-row');
if (!row) {
return;
}
row.remove();
}
function downloadJsonFile(fileName, value) {
var blob = new Blob([JSON.stringify(value, null, 2)], { type: 'application/json;charset=utf-8' });
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = fileName;
link.click();
URL.revokeObjectURL(url);
}
async function exportWorkspaceSnapshot() {
if (!workspaceFormState.workspaceId || !window.CrankApi) {
return;
}
try {
var snapshot = await window.CrankApi.exportWorkspace(workspaceFormState.workspaceId);
var slug = workspaceFormState.workspaceRecord && workspaceFormState.workspaceRecord.workspace
? workspaceFormState.workspaceRecord.workspace.slug
: tKey('settings.nav.workspace');
downloadJsonFile(slug + '-snapshot.json', snapshot);
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace_setup.export_error'), tKey('workspace_setup.export_error_title'));
}
}
}
async function deleteWorkspaceAction() {
if (!workspaceFormState.workspaceId || !window.CrankApi || !window.CrankAuth) {
return;
}
if (!confirm(tKey('workspace_setup.delete_confirm'))) {
return;
}
try {
await window.CrankApi.deleteWorkspace(workspaceFormState.workspaceId);
await window.CrankAuth.logout();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace_setup.delete_error'), tKey('workspace_setup.delete_error_title'));
}
}
}
async function initPage() {
updatePageMode();
document.getElementById('export-workspace-btn').addEventListener('click', exportWorkspaceSnapshot);
document.getElementById('delete-workspace-btn').addEventListener('click', deleteWorkspaceAction);
if (workspaceFormState.isCreateMode) {
document.getElementById('ws-name').focus();
return;
}
await window.whenWorkspacesReady();
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
if (!workspace || !window.CrankApi) {
return;
}
try {
var record = await window.CrankApi.getWorkspace(workspace.id);
applyWorkspaceRecord(record);
await loadWorkspaceAccessData();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to load workspace', 'Workspace load failed');
}
}
}
document.addEventListener('DOMContentLoaded', initPage);
window.addEventListener('beforeunload', function (event) {
if (workspaceFormState.formDirty) {
event.preventDefault();
event.returnValue = '';
}
});
+287
View File
@@ -0,0 +1,287 @@
var WS_LIST = [
{ id: 'ws_default', slug: 'default', name: 'Default workspace', role: 'Owner', letter: 'D', color: '#0d9488' }
];
var workspaceLoadPromise = null;
var workspaceLoadFailed = false;
var lastWorkspaceId = null;
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : key;
}
function buildIconSvg(href, width, height) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', String(width));
svg.setAttribute('height', String(height));
var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', href);
svg.appendChild(use);
return svg;
}
function workspaceColor(index) {
return ['#0d9488', '#7c3aed', '#0891b2', '#f59e0b', '#2563eb'][index % 5];
}
function mapWorkspace(record, index) {
var workspace = record && record.workspace ? record.workspace : record;
var displayName = workspace.display_name || workspace.slug || workspace.id;
var settings = workspace.settings || {};
var role = record && record.role
? String(record.role).replace(/^\w/, function(char) { return char.toUpperCase(); })
: tKey('workspace_setup.role.owner');
return {
id: workspace.id,
slug: workspace.slug,
name: displayName,
role: role,
letter: displayName.charAt(0).toUpperCase(),
color: settings.color || workspaceColor(index),
status: workspace.status,
settings: settings,
};
}
function cachedWorkspaceId() {
try {
return localStorage.getItem('crank_workspace_id');
} catch (_error) {
return null;
}
}
function cachedWorkspaceSlug() {
try {
return localStorage.getItem('crank_workspace_slug');
} catch (_error) {
return null;
}
}
function sessionWorkspaceId() {
if (!window.CrankAuth || typeof window.CrankAuth.getCachedSession !== 'function') {
return null;
}
var session = window.CrankAuth.getCachedSession();
return session && session.current_workspace_id ? session.current_workspace_id : null;
}
function cacheCurrentWorkspace(workspace) {
if (!workspace) return;
try {
localStorage.setItem('crank_workspace_id', workspace.id);
localStorage.setItem('crank_workspace_slug', workspace.slug);
} catch (_error) {}
}
function resolveCurrentWorkspace() {
var workspaceId = sessionWorkspaceId() || cachedWorkspaceId();
var workspaceSlug = cachedWorkspaceSlug();
return WS_LIST.find(function(item) { return item.id === workspaceId; })
|| WS_LIST.find(function(item) { return item.slug === workspaceSlug; })
|| WS_LIST[0]
|| null;
}
function emitWorkspaceChange(workspace) {
if (!workspace) {
return;
}
lastWorkspaceId = workspace.id;
window.dispatchEvent(new CustomEvent('crank:workspacechange', { detail: workspace }));
}
function getCurrentWs() {
var workspace = resolveCurrentWorkspace();
if (workspace) {
cacheCurrentWorkspace(workspace);
}
return workspace;
}
function updateWorkspaceHeader(workspace) {
var nameEl = document.getElementById('ws-current-name');
var dotEl = document.getElementById('ws-dot');
if (nameEl) nameEl.textContent = workspace ? workspace.name : 'No workspace';
if (dotEl && workspace) {
dotEl.textContent = workspace.letter;
dotEl.style.background = workspace.color;
}
}
function renderWorkspaceList() {
var current = getCurrentWs();
updateWorkspaceHeader(current);
var list = document.getElementById('ws-dropdown-list');
if (!list) return;
window.CrankDom.clear(list);
WS_LIST.forEach(function(workspace) {
var active = current && workspace.id === current.id;
var item = document.createElement('div');
item.className = 'ws-dropdown-item' + (active ? ' active' : '');
item.addEventListener('click', function() {
switchWorkspace(workspace.id);
});
var dot = document.createElement('div');
dot.className = 'ws-item-dot';
dot.style.background = workspace.color;
dot.textContent = workspace.letter;
item.appendChild(dot);
var info = document.createElement('div');
info.className = 'ws-item-info';
var name = document.createElement('div');
name.className = 'ws-item-name';
name.textContent = workspace.name;
var role = document.createElement('div');
role.className = 'ws-item-role';
role.textContent = workspace.role;
info.appendChild(name);
info.appendChild(role);
item.appendChild(info);
if (active) {
var check = buildIconSvg(
(window.APP_BASE || '') + 'icons/general/check.svg#icon',
12,
12
);
item.appendChild(check);
}
list.appendChild(item);
});
var divider = document.createElement('div');
divider.className = 'ws-dropdown-divider';
list.appendChild(divider);
var manageLink = document.createElement('a');
manageLink.className = 'ws-dropdown-mgmt-link';
manageLink.href = (window.CrankRoutes && window.CrankRoutes.workspaceSetup) || '/workspace-setup';
manageLink.addEventListener('click', function() {
var dd = document.getElementById('ws-dropdown');
if (dd) dd.hidden = true;
});
manageLink.appendChild(
buildIconSvg(
(window.APP_BASE || '') + 'icons/general/settings.svg#icon',
14,
14
)
);
manageLink.appendChild(document.createTextNode(tKey('settings.ws.title')));
list.appendChild(manageLink);
}
async function loadWorkspaces() {
if (workspaceLoadPromise) return workspaceLoadPromise;
workspaceLoadPromise = (async function() {
if (!window.CrankApi) {
renderWorkspaceList();
return WS_LIST;
}
try {
var response = await window.CrankApi.listWorkspaces();
var items = (response && response.items ? response.items : []).map(mapWorkspace);
if (items.length > 0) {
WS_LIST = items;
}
workspaceLoadFailed = false;
} catch (_error) {
workspaceLoadFailed = true;
}
renderWorkspaceList();
return WS_LIST;
}());
return workspaceLoadPromise;
}
async function refreshWorkspaces() {
workspaceLoadPromise = null;
return loadWorkspaces();
}
function initWorkspaceSwitcher() {
renderWorkspaceList();
loadWorkspaces();
}
function toggleWsSwitcher(e) {
e.stopPropagation();
var dd = document.getElementById('ws-dropdown');
if (dd) dd.hidden = !dd.hidden;
}
function switchWorkspace(workspaceId) {
var workspace = WS_LIST.find(function(item) { return item.id === workspaceId; });
if (!workspace) return;
var dd = document.getElementById('ws-dropdown');
if (dd) dd.hidden = true;
if (!window.CrankApi || !window.CrankAuth || typeof window.CrankAuth.replaceSession !== 'function') {
cacheCurrentWorkspace(workspace);
renderWorkspaceList();
emitWorkspaceChange(workspace);
return;
}
return window.CrankApi
.setCurrentWorkspace(workspace.id)
.then(function(session) {
window.CrankAuth.replaceSession(session);
cacheCurrentWorkspace(workspace);
renderWorkspaceList();
emitWorkspaceChange(workspace);
return workspace;
})
.catch(function(error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace.switch_error'), tKey('workspace.switch_error_title'));
}
throw error;
});
}
function setCurrentWorkspace(workspace) {
if (!workspace) {
return Promise.resolve(null);
}
return Promise.resolve(switchWorkspace(workspace.id));
}
window.getCurrentWorkspace = getCurrentWs;
window.whenWorkspacesReady = loadWorkspaces;
window.refreshWorkspaces = refreshWorkspaces;
window.setCurrentWorkspace = setCurrentWorkspace;
window.hasWorkspaceApiFailure = function() { return workspaceLoadFailed; };
document.addEventListener('DOMContentLoaded', initWorkspaceSwitcher);
window.addEventListener('crank:sessionchange', function() {
var workspace = getCurrentWs();
renderWorkspaceList();
if (workspace && workspace.id !== lastWorkspaceId) {
emitWorkspaceChange(workspace);
}
});
document.addEventListener('click', function(e) {
if (!e.target.closest('#ws-switcher')) {
var dd = document.getElementById('ws-dropdown');
if (dd) dd.hidden = true;
}
});