Polish community UI copy and cleanup
Deploy / deploy (push) Successful in 1m36s
CI / Rust Checks (push) Successful in 4m50s
CI / UI Checks (push) Successful in 5s
CI / Deployment Manifests (push) Successful in 2s
CI / Frontend E2E (push) Failing after 5m5s

This commit is contained in:
github-ops
2026-06-19 21:15:02 +00:00
parent 66dd0deee5
commit d072d142ca
57 changed files with 710 additions and 6773 deletions
+6 -9
View File
@@ -41,9 +41,6 @@
<div class="nav-right">
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
<svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg>
</button>
<div class="nav-divider"></div>
<div class="user-menu" @click.stop>
<div class="nav-avatar" @click="openDropdown = openDropdown === 'user' ? null : 'user'" data-i18n-title="nav.account" title="Account">AT</div>
@@ -82,7 +79,7 @@
<div class="page-header">
<div>
<h1 class="page-heading" data-i18n="agents.title">Agents</h1>
<p class="page-subheading" x-text="subtitleText()">Named MCP endpoints that expose a curated subset of operations to an LLM</p>
<p class="page-subheading" x-text="subtitleText()">Group your MCP tools by AI agent.</p>
</div>
<button class="btn-new" @click="openCreate()">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
@@ -136,7 +133,7 @@
<!-- Info callout -->
<div class="agents-info-callout">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="var(--accent)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="7"/><path d="M8 11V8M8 5v-.5"/></svg>
<span x-text="agentCalloutText()">Each agent gets its own MCP endpoint.</span>
<span x-text="agentCalloutText()">Group MCP tools around concrete tasks for a concrete agent. Do not give one LLM too many tools at once: it can increase mistakes and hallucinations. Each agent has its own API keys.</span>
</div>
<!-- Loading -->
@@ -157,7 +154,7 @@
</div>
<div class="empty-state-title" data-i18n="agents.empty.initial.title">No agents yet</div>
<div class="empty-state-sub" x-text="emptyStateText()">Create your first agent to get a dedicated MCP endpoint with a curated set of tools.</div>
<div class="empty-state-sub" style="margin-top:8px;" x-text="tKey('agents.empty.initial.next_step')">After creation, issue separate machine-access keys for this endpoint on the API Keys page.</div>
<div class="empty-state-sub" style="margin-top:8px;" x-text="tKey('agents.empty.initial.next_step')">After creating an agent, issue an API key for it on the API Keys page.</div>
<button class="btn-new" @click="openCreate()" style="margin-top: 16px;">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
<span data-i18n="agents.new">New agent</span>
@@ -287,12 +284,12 @@
<div class="form-hint" x-show="form.slug">
<span data-i18n="agents.drawer.endpoint">MCP endpoint</span>: <code style="font-family:monospace;font-size:11px;color:var(--accent)" x-text="'/mcp/v1/' + (localStorage.getItem('crank_workspace_slug') || 'default') + '/' + form.slug"></code>
</div>
<div class="form-hint" data-i18n="agents.drawer.slug_hint">Keep the slug stable after clients start using this endpoint. Changing it changes the MCP path.</div>
<div class="form-hint" data-i18n="agents.drawer.slug_hint">Slug is used as part of the endpoint to identify the agent.</div>
</div>
<div class="form-group" style="margin-bottom: 14px;">
<label class="form-label"><span data-i18n="agents.drawer.description">Description</span> <span class="form-label-optional" data-i18n="agents.drawer.optional">(optional)</span></label>
<textarea class="form-textarea" rows="3" data-i18n-ph="agents.drawer.placeholder.description" placeholder="What does this agent do? What LLM or use-case is it for?"
<textarea class="form-textarea" rows="3" data-i18n-ph="agents.drawer.placeholder.description" placeholder="What does this agent do? What scenario is it for?"
x-model="form.description"></textarea>
</div>
@@ -318,7 +315,7 @@
<div class="drawer-section-title" data-i18n="agents.drawer.operations">Operations</div>
<span class="drawer-section-count" x-text="tfKey('agents.drawer.selected', { count: form.selectedOps.length })"></span>
</div>
<div class="drawer-section-sub" x-text="operationsSubText()">Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.</div>
<div class="drawer-section-sub" x-text="operationsSubText()">Select the MCP tools available to this agent.</div>
<div class="ops-picker">
<!-- Search -->
+6 -32
View File
@@ -59,11 +59,6 @@
<div class="nav-right">
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
<svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg>
</button>
<div class="nav-divider"></div>
<div class="user-menu">
@@ -102,7 +97,7 @@
<div class="page-header">
<div class="page-header-text">
<h1 class="page-title" data-i18n="apikeys.title">Agent Keys</h1>
<p class="page-subtitle" data-i18n="apikeys.subtitle">Keys authenticate external MCP clients against a specific AI agent endpoint.</p>
<p class="page-subtitle" data-i18n="apikeys.subtitle">These keys connect an MCP client to the MCP server and are issued for a specific agent.</p>
</div>
<div class="page-header-actions">
<button class="btn-primary" id="btn-create-key" type="button">
@@ -126,8 +121,8 @@
<div class="section-card">
<div class="section-card-header">
<div>
<div class="section-card-title" data-i18n="apikeys.agent.title">Agent access</div>
<div class="section-card-subtitle" id="agent-access-subtitle" data-i18n="apikeys.agent.subtitle">Select the AI agent whose MCP endpoint should accept this key.</div>
<div class="section-card-title" data-i18n="apikeys.agent.title">Agent selection</div>
<div class="section-card-subtitle" id="agent-access-subtitle" data-i18n="apikeys.agent.subtitle">Select the AI agent this key is issued for.</div>
</div>
</div>
<div class="section-card-body" style="display:grid;gap:12px;">
@@ -139,27 +134,6 @@
</div>
</div>
<div class="section-card">
<div class="section-card-header">
<div>
<div class="section-card-title" id="machine-access-title" data-i18n="apikeys.machine_access.title">Machine access modes</div>
<div class="section-card-subtitle" id="machine-access-subtitle" data-i18n="apikeys.machine_access.subtitle">This build exposes the stable machine-access contract and shows which modes are available now.</div>
</div>
</div>
<div class="section-card-body" style="display:grid;gap:10px;">
<div style="font-size:13px;color:var(--text-secondary);line-height:1.65;" id="machine-access-summary" data-testid="machine-access-summary">
Community currently supports static AI-agent keys.
</div>
<div class="callout info">
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--blue)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="8" r="6.5"/>
<path d="M8 11V8M8 5.5V5"/>
</svg>
<div id="machine-access-note" data-i18n="apikeys.machine_access.note">Short-lived and one-time token issuance uses the same public HTTP surface, but requires a commercial edition.</div>
</div>
</div>
</div>
<div class="section-card">
<div class="section-card-header">
<div>
@@ -178,7 +152,7 @@
<tr>
<th data-i18n="apikeys.th.name">Name</th>
<th data-i18n="apikeys.th.prefix">Key prefix</th>
<th data-i18n="apikeys.th.scopes">Scopes</th>
<th data-i18n="apikeys.th.scopes">Access</th>
<th data-i18n="apikeys.th.created">Created</th>
<th data-i18n="apikeys.th.last">Last used</th>
<th data-i18n="apikeys.th.status">Status</th>
@@ -194,7 +168,7 @@
<div class="section-card">
<div class="section-card-header">
<div class="section-card-title" data-i18n="apikeys.scope_ref">Scope reference</div>
<div class="section-card-title" data-i18n="apikeys.scope_ref">Access reference</div>
</div>
<div class="section-card-body" style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">
<div>
@@ -238,7 +212,7 @@
<div class="field-hint" data-i18n="apikeys.modal.name_hint">A descriptive label to identify the key. Only visible to admins.</div>
</div>
<div class="field-group" style="margin-bottom:0;">
<label class="field-label" data-i18n="apikeys.modal.scopes">Scopes</label>
<label class="field-label" data-i18n="apikeys.modal.scopes">Access</label>
<div style="display:flex;flex-direction:column;gap:8px;margin-top:2px;" id="scope-checkboxes">
</div>
</div>
+1 -4
View File
@@ -38,9 +38,6 @@
</div>
<div class="nav-right">
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
<svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg>
</button>
<div class="nav-divider"></div>
<div class="user-menu">
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
@@ -77,7 +74,7 @@
<div class="page-header">
<div class="page-header-text">
<h1 class="page-title" data-i18n="logs.title">Logs</h1>
<p class="page-subtitle" data-i18n="logs.subtitle">Real-time invocation log for all operations in this workspace.</p>
<p class="page-subtitle" data-i18n="logs.subtitle">Invocation log for all operations in this workspace.</p>
</div>
</div>
+8 -9
View File
@@ -53,7 +53,6 @@
<div class="nav-right">
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications"><svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg></button>
<div class="nav-divider"></div>
<div class="user-menu">
<div class="nav-avatar" data-testid="shell-avatar" data-i18n-title="nav.account" title="Account">AT</div>
@@ -80,7 +79,7 @@
<div class="page-header">
<div class="page-header-text">
<h1 class="page-title" data-i18n="secrets.title">Secrets</h1>
<p class="page-subtitle" data-i18n="secrets.subtitle">Encrypted credentials for upstream REST APIs.</p>
<p class="page-subtitle" data-i18n="secrets.subtitle">Credentials for authorization on target API hosts.</p>
</div>
<div class="page-header-actions">
<button class="btn-primary" id="btn-create-secret" data-testid="secret-create-button" type="button">
@@ -96,8 +95,8 @@
<path d="M8 11V8M8 5.5V5"/>
</svg>
<div>
<strong data-i18n="secrets.callout.title">Secret plaintext is write-only.</strong>
<span data-i18n="secrets.callout.body">Crank encrypts secret values with the workspace master key. After create or rotate, the API returns only metadata, so this page focuses on lifecycle and usage references.</span>
<strong data-i18n="secrets.callout.title">Secret values are protected.</strong>
<span data-i18n="secrets.callout.body">After saving, secrets are encrypted with the workspace key and cannot be read back. A secret can be rotated or deleted.</span>
</div>
</div>
@@ -148,7 +147,7 @@
<div class="field-group">
<label class="field-label" data-i18n="secrets.modal.name">Secret name</label>
<input class="field-input" id="secret-name" data-testid="secret-name-input" type="text" autocomplete="off">
<div class="field-hint" data-i18n="secrets.modal.name_hint">Use a stable operator-facing label. Plaintext values are never returned after storage.</div>
<div class="field-hint" data-i18n="secrets.modal.name_hint">Use a clear name so you can select this secret in operation settings later.</div>
</div>
<div class="field-group">
@@ -164,7 +163,7 @@
<div class="secret-value-grid" id="secret-value-fields">
<div class="field-group" data-kind-field="string">
<label class="field-label" id="secret-string-label" data-i18n="secrets.modal.value">Secret value</label>
<label class="field-label" id="secret-string-label" data-i18n="secrets.modal.value">Token value</label>
<input class="field-input" id="secret-string-value" data-testid="secret-value-input" type="text" autocomplete="off">
</div>
@@ -180,13 +179,13 @@
</div>
<div class="field-group" data-kind-field="json" hidden>
<label class="field-label" data-i18n="secrets.modal.json_payload">JSON payload</label>
<label class="field-label" data-i18n="secrets.modal.json_payload">JSON</label>
<textarea class="field-textarea code-textarea" id="secret-json-value" data-testid="secret-json-input" rows="8" spellcheck="false"></textarea>
<div class="field-hint" data-i18n="secrets.modal.json_hint">Useful for generic secret payloads. The value must be valid JSON.</div>
<div class="field-hint" data-i18n="secrets.modal.json_hint">Enter valid JSON. Use this type when an API needs several related values.</div>
</div>
</div>
<div class="secret-inline-note" id="secret-modal-note" data-i18n="secrets.modal.create_note">Creation stores encrypted plaintext and returns metadata only.</div>
<div class="secret-inline-note" id="secret-modal-note" data-i18n="secrets.modal.create_note">After saving, the value is encrypted and will no longer be displayed.</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" id="secret-modal-cancel-btn" type="button" data-i18n="secrets.modal.cancel">Cancel</button>
+2 -50
View File
@@ -38,9 +38,6 @@
</div>
<div class="nav-right">
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
<svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg>
</button>
<div class="nav-divider"></div>
<div class="user-menu">
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
@@ -77,7 +74,6 @@
<div class="page-header" style="margin-bottom: 24px;">
<div class="page-header-text">
<h1 class="page-title" data-i18n="settings.page.title">Account settings</h1>
<p class="page-subtitle" data-i18n="settings.page.subtitle">Manage the live profile, password and workspace settings available in this build. Planned capabilities stay visible, but they do not pretend to be wired yet.</p>
</div>
</div>
@@ -104,13 +100,6 @@
</svg>
<span data-i18n="settings.nav.security">Security</span>
</button>
<button class="settings-nav-item" data-section="notifications">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 1.5a5 5 0 015 5v2.5l1 2H2l1-2V6.5a5 5 0 015-5z"/>
<path d="M6.5 13a1.5 1.5 0 003 0"/>
</svg>
<span data-i18n="settings.nav.notif">Notifications</span>
</button>
</nav>
<!-- ── Content ── -->
@@ -128,7 +117,7 @@
<div class="field-group">
<label class="field-label" data-i18n="settings.ws.name">Workspace name</label>
<input class="field-input" id="settings-ws-slug" type="text" value="acme-workspace" autocomplete="off">
<div class="field-hint" data-i18n="settings.ws.slug_hint">Used in API endpoints and across the console. Only lowercase letters, numbers and hyphens.</div>
<div class="field-hint" data-i18n="settings.ws.slug_hint">Used in API endpoints and in the interface. Only lowercase English letters, numbers and hyphens are allowed.</div>
</div>
<div class="field-group">
<label class="field-label" data-i18n="settings.ws.display">Display name</label>
@@ -136,7 +125,7 @@
</div>
<div class="field-group">
<label class="field-label"><span data-i18n="settings.ws.description_optional">Description (optional)</span></label>
<textarea class="field-textarea" id="settings-ws-description" rows="2" data-i18n-ph="settings.ws.description_placeholder" placeholder="What does this workspace do?">Primary production workspace for CRM and e-commerce API operations.</textarea>
<textarea class="field-textarea" id="settings-ws-description" rows="2" data-i18n-ph="settings.ws.description_placeholder" placeholder="What tasks does this workspace perform?">Primary production workspace for CRM and e-commerce API operations.</textarea>
</div>
<div style="display:flex;justify-content:flex-end;padding-top:4px;">
@@ -161,8 +150,6 @@
<div class="field-hint" data-i18n="settings.profile.avatar_hint">Your avatar is generated from your display name and email.</div>
</div>
</div>
<div class="field-hint" style="margin-bottom:16px;" data-i18n="settings.profile.backend_hint">Your name and email are stored on the backend and used across the console and session identity.</div>
<div class="field-row">
<div class="field-group">
<label class="field-label" data-i18n="settings.profile.first_name">First name</label>
@@ -229,21 +216,6 @@
</div>
</div>
<div style="padding: 4px 20px 16px;">
<div class="settings-section-divider" style="margin-top:0;"></div>
<div class="planned-section-title">
<span data-i18n="settings.security.capabilities_title">More security options</span>
</div>
<div class="callout warning" style="margin-bottom:0;">
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--amber)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<polygon points="8,1.5 15.5,14.5 0.5,14.5" fill="none"/><path d="M8 6v4M8 11.5v.5"/>
</svg>
<div>
<strong id="settings-security-capability-title" data-i18n="settings.security.not_available">Available later.</strong> <span id="settings-security-capability-body" data-i18n="settings.security.capabilities_body">Two-factor authentication, passkeys, and multi-session controls will be added later. The current live flow uses password sign-in with one HttpOnly browser session.</span>
</div>
</div>
</div>
<div data-slot="settings.sso_panel"></div>
<div data-slot="settings.totp_panel"></div>
<div data-slot="settings.audit_panel"></div>
@@ -265,26 +237,6 @@
</div>
</div>
<!-- ■ NOTIFICATIONS ■ -->
<div id="section-notifications" class="section-anchor">
<div class="section-card" style="margin-bottom: 20px;">
<div class="section-card-header">
<div class="section-card-title" data-i18n="settings.notifications.title">Notifications</div>
</div>
<div style="padding: 8px 20px 16px;">
<div class="callout info" style="margin-bottom:14px;">
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--blue)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="8" r="6.5"/>
<path d="M8 11V8M8 5.5V5"/>
</svg>
<div>
<strong id="settings-notifications-capability-title" data-i18n="settings.notifications.not_ready_title">Not included in this build.</strong> <span id="settings-notifications-capability-body" data-i18n="settings.notifications.not_ready_body">Notification routing and personal preferences are outside the current Community surface.</span>
</div>
</div>
</div>
</div>
</div>
</div><!-- /settings-content -->
</div><!-- /settings-layout -->
-3
View File
@@ -45,9 +45,6 @@
</div>
<div class="nav-right">
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
<svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg>
</button>
<div class="nav-divider"></div>
<div class="user-menu">
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
+4 -9
View File
@@ -40,11 +40,6 @@
<div class="nav-right">
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
<svg width="15" height="15"><use href="../../icons/general/bell.svg#icon"/></svg>
</button>
<div class="nav-divider"></div>
<div class="user-menu">
@@ -140,7 +135,7 @@
<div class="step-indicator">3</div>
<div class="step-content">
<div class="step-number" data-step="3">Step 3</div>
<div class="step-name" id="sidebar-step-3-name">Request config</div>
<div class="step-name" id="sidebar-step-3-name">Настройки запроса</div>
<div class="step-status-text" data-i18n="wizard.status.not_started">Not started</div>
</div>
</div>
@@ -149,7 +144,7 @@
<div class="step-indicator">4</div>
<div class="step-content">
<div class="step-number" data-step="4">Step 4</div>
<div class="step-name" data-i18n="wizard.step_name.tool">Tool config</div>
<div class="step-name" data-i18n="wizard.step_name.tool">Настройки инструмента</div>
<div class="step-status-text" data-i18n="wizard.status.not_started">Not started</div>
</div>
</div>
@@ -158,7 +153,7 @@
<div class="step-indicator">5</div>
<div class="step-content">
<div class="step-number" data-step="5">Step 5</div>
<div class="step-name" data-i18n="wizard.step_name.mapping">Mapping</div>
<div class="step-name" data-i18n="wizard.step_name.mapping">Связи полей</div>
<div class="step-status-text" data-i18n="wizard.status.not_started">Not started</div>
</div>
</div>
@@ -241,7 +236,7 @@
<div class="field-group">
<label class="field-label" data-i18n="wizard.step2.quick_secret_value">Secret value</label>
<input class="field-input" id="quick-secret-value" data-testid="wizard-quick-secret-value" type="text" autocomplete="off">
<div class="field-hint" data-i18n="wizard.step2.quick_secret_hint">The plaintext is encrypted immediately and will not be returned again.</div>
<div class="field-hint" data-i18n="wizard.step2.quick_secret_hint">The value is encrypted immediately and will not be shown again.</div>
</div>
</div>
<div class="modal-footer">
+6 -6
View File
@@ -5,8 +5,8 @@
<div class="step-panel-eyebrow-line"></div>
<span data-step-counter>Step 1 of 5</span>
</div>
<h1 class="step-panel-title" data-i18n="wizard.step1.title">Choose a protocol</h1>
<p class="step-panel-subtitle" data-i18n="wizard.step1.subtitle">Select the transport protocol your upstream service uses. This determines how Crank will communicate with your API and which configuration options are available in subsequent steps.</p>
<h1 class="step-panel-title" data-i18n="wizard.step1.title">Выберите протокол</h1>
<p class="step-panel-subtitle" data-i18n="wizard.step1.subtitle">Выберите протокол вашего API. От этого зависит, как Crank будет выполнять запросы и какие настройки будут доступны на следующих шагах.</p>
</div>
<!-- Protocol selection cards -->
@@ -24,7 +24,7 @@
</svg>
</div>
<div class="protocol-card-name" data-i18n="wizard.step1.rest_name">REST / HTTP</div>
<div class="protocol-card-tagline" data-i18n="wizard.step1.rest_tagline">Standard HTTP verbs over a RESTful endpoint. The most broadly supported option.</div>
<div class="protocol-card-tagline" data-i18n="wizard.step1.rest_tagline">Стандартные HTTP-методы для REST API. Самый универсальный вариант.</div>
<div class="protocol-card-tags">
<span class="protocol-tag">GET</span>
<span class="protocol-tag">POST</span>
@@ -48,9 +48,9 @@
<path d="M8 6v4M8 11.5v.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title" data-i18n="wizard.step1.tip_title">You can change this later</div>
<div class="info-callout-title" data-i18n="wizard.step1.tip_title">Это можно изменить позже</div>
<div class="info-callout-text">
<span data-i18n="wizard.step1.tip_body">Switching protocol after configuring subsequent steps will clear schema and mapping data. Save your operation as a draft before experimenting. See protocol migration guide for details.</span>
<span data-i18n="wizard.step1.tip_body">Если сменить протокол после настройки следующих шагов, схемы и связи полей будут очищены. Перед экспериментами сохраните операцию.</span>
</div>
</div>
</div>
@@ -61,7 +61,7 @@
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title" data-i18n="wizard.step1.community_protocol_title">Community protocol scope</div>
<div class="info-callout-title" data-i18n="wizard.step1.community_protocol_title">Ограничение Community версии</div>
<div class="info-callout-text" id="wizard-edition-protocol-note-text"></div>
</div>
</div>
+46 -46
View File
@@ -1,12 +1,12 @@
<!-- ════════════ STEP 2: Upstream target ════════════ -->
<!-- ════════════ STEP 2: API host ════════════ -->
<div id="step-panel-2" class="step-pane">
<div class="step-panel-header">
<div class="step-panel-eyebrow">
<div class="step-panel-eyebrow-line"></div>
<span data-step-counter>Step 2 of 5</span>
</div>
<h1 class="step-panel-title" data-i18n="wizard.step2.title">Select upstream &amp; endpoint</h1>
<p class="step-panel-subtitle" data-i18n="wizard.step2.subtitle">Choose an existing upstream — a shared host with its auth config — or register a new one. Then define the specific endpoint path this operation will call.</p>
<h1 class="step-panel-title" data-i18n="wizard.step2.title">Выберите API-хост и путь</h1>
<p class="step-panel-subtitle" data-i18n="wizard.step2.subtitle">Выберите существующий API-хост или добавьте новый. Затем задайте путь, который будет вызывать эта операция.</p>
</div>
<!-- ── Upstream selector ── -->
@@ -21,10 +21,10 @@
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step2.upstream_title">Upstream</div>
<div class="config-card-subtitle" data-i18n="wizard.step2.upstream_subtitle">Shared host, base URL, and authentication</div>
<div class="config-card-title" data-i18n="wizard.step2.upstream_title">API-хост</div>
<div class="config-card-subtitle" data-i18n="wizard.step2.upstream_subtitle">Базовый URL и способ авторизации</div>
</div>
<span class="config-card-optional" style="color: var(--error, #f87171);" data-i18n="wizard.required">Required</span>
<span class="config-card-optional" style="color: var(--error, #f87171);" data-i18n="wizard.required">Обязательно</span>
</div>
<div class="config-card-body" style="gap: 12px; padding: 16px;">
@@ -32,7 +32,7 @@
<div class="upstream-combobox" id="upstream-combobox">
<div class="upstream-combobox-trigger" id="upstream-combobox-trigger" onclick="toggleUpstreamDropdown(event)">
<div class="upstream-combobox-value" id="upstream-combobox-value">
<span class="upstream-combobox-placeholder" data-i18n="wizard.step2.upstream_placeholder">Select an upstream</span>
<span class="upstream-combobox-placeholder" data-i18n="wizard.step2.upstream_placeholder">Выберите API-хост</span>
</div>
<svg class="upstream-combobox-chevron" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="var(--text-muted)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 6l4 4 4-4"/>
@@ -45,7 +45,7 @@
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-muted)" stroke-width="1.8" stroke-linecap="round">
<circle cx="7" cy="7" r="5"/><path d="M12 12l2.5 2.5"/>
</svg>
<input class="upstream-search-input" id="upstream-search" type="text" data-i18n-ph="wizard.step2.search_placeholder" placeholder="Search by name or URL…" oninput="filterUpstreams(this.value)" autocomplete="off" spellcheck="false">
<input class="upstream-search-input" id="upstream-search" type="text" data-i18n-ph="wizard.step2.search_placeholder" placeholder="Поиск по имени или URL…" oninput="filterUpstreams(this.value)" autocomplete="off" spellcheck="false">
</div>
<div class="upstream-dropdown-list" id="upstream-dropdown-list">
<!-- populated by JS -->
@@ -58,7 +58,7 @@
<div class="upstream-preview-name" id="upstream-preview-name"></div>
<div class="upstream-preview-url" id="upstream-preview-url"></div>
<span class="upstream-auth-badge" id="upstream-preview-badge"></span>
<button class="upstream-preview-change" onclick="beginEditSelectedUpstream(event)" data-i18n="wizard.step2.change">Change</button>
<button class="upstream-preview-change" onclick="beginEditSelectedUpstream(event)" data-i18n="wizard.step2.change">Изменить</button>
</div>
<!-- Register new upstream trigger row -->
@@ -69,7 +69,7 @@
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="var(--accent)" stroke-width="2.2" stroke-linecap="round">
<path d="M8 3v10M3 8h10"/>
</svg>
<span data-i18n="wizard.step2.register_new">Register new upstream</span>
<span data-i18n="wizard.step2.register_new">Добавить новый API-хост</span>
</div>
<!-- Register new upstream form (kept as-is per design) -->
@@ -77,86 +77,86 @@
<div class="upstream-new-form-inner">
<div class="form-row" style="grid-template-columns: 1fr 2fr;">
<div class="form-group">
<label class="form-label"><span data-i18n="wizard.step2.name">Name</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
<label class="form-label"><span data-i18n="wizard.step2.name">Имя</span> <span class="form-label-required" data-i18n="workspace_setup.required">обязательно</span></label>
<input class="form-input" id="new-upstream-name" type="text" placeholder="e.g. acme-api" autocomplete="off" spellcheck="false">
<div class="form-hint" data-i18n="wizard.step2.unique_hint">Unique identifier for this upstream.</div>
<div class="form-hint" data-i18n="wizard.step2.unique_hint">Уникальное имя этого API-хоста.</div>
</div>
<div class="form-group">
<label class="form-label"><span data-i18n="wizard.step2.base_url">Base URL</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
<label class="form-label"><span data-i18n="wizard.step2.base_url">Базовый URL</span> <span class="form-label-required" data-i18n="workspace_setup.required">обязательно</span></label>
<input class="form-input input-mono" id="new-upstream-url" type="text" placeholder="https://api.example.com" autocomplete="off" spellcheck="false">
<div class="form-hint" data-i18n="wizard.step2.base_url_hint">Root URL without a trailing slash. Auth is configured separately below.</div>
<div class="form-hint" data-i18n="wizard.step2.base_url_hint">Корневой URL без завершающего слеша. Авторизация настраивается ниже.</div>
</div>
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step2.auth_selector">Upstream auth</label>
<label class="form-label" data-i18n="wizard.step2.auth_selector">Авторизация API-хоста</label>
<select class="form-select" id="new-upstream-auth-mode" data-testid="wizard-auth-mode-select" onchange="updateUpstreamAuthUi()">
<option value="none" data-i18n="wizard.step2.auth_mode.none">No auth</option>
<option value="existing" data-i18n="wizard.step2.auth_mode.existing">Use existing auth profile</option>
<option value="create" data-i18n="wizard.step2.auth_mode.create">Create auth profile now</option>
<option value="none" data-i18n="wizard.step2.auth_mode.none">Без авторизации</option>
<option value="existing" data-i18n="wizard.step2.auth_mode.existing">Использовать существующий профиль авторизации</option>
<option value="create" data-i18n="wizard.step2.auth_mode.create">Создать профиль авторизации сейчас</option>
</select>
<div class="form-hint" data-i18n="wizard.step2.auth_selector_hint">Use secrets-backed auth profiles instead of embedding credential headers in the upstream definition.</div>
<div class="form-hint" data-i18n="wizard.step2.auth_selector_hint">Используйте секреты, чтобы не хранить токены и пароли прямо в настройках API-хоста.</div>
</div>
<div class="form-group" id="upstream-auth-existing-group" hidden>
<label class="form-label" data-i18n="wizard.step2.auth_profile">Auth profile</label>
<label class="form-label" data-i18n="wizard.step2.auth_profile">Профиль авторизации</label>
<select class="form-select" id="new-upstream-auth-profile" data-testid="wizard-auth-profile-select"></select>
<div class="form-hint" id="new-upstream-auth-profile-hint" data-i18n="wizard.step2.auth_profile_hint">Selected profile will be resolved at runtime and attached through `execution_config.auth_profile_ref`.</div>
<div class="form-hint" id="new-upstream-auth-profile-hint" data-i18n="wizard.step2.auth_profile_hint">Выбранный профиль применяется автоматически при каждом вызове инструмента.</div>
</div>
<div class="config-card" id="upstream-auth-create-group" hidden style="margin-top: 6px;">
<div class="config-card-header">
<div>
<div class="config-card-title" data-i18n="wizard.step2.create_profile_title">Create auth profile</div>
<div class="config-card-subtitle" data-i18n="wizard.step2.create_profile_subtitle">Create a reusable profile backed by encrypted workspace secrets.</div>
<div class="config-card-title" data-i18n="wizard.step2.create_profile_title">Создать профиль авторизации</div>
<div class="config-card-subtitle" data-i18n="wizard.step2.create_profile_subtitle">Профиль можно переиспользовать в нескольких API-хостах. Значения берутся из зашифрованных секретов воркспейса.</div>
</div>
</div>
<div class="config-card-body" style="gap: 14px;">
<div class="form-group">
<label class="form-label"><span data-i18n="wizard.step2.profile_name">Profile name</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
<label class="form-label"><span data-i18n="wizard.step2.profile_name">Имя профиля</span> <span class="form-label-required" data-i18n="workspace_setup.required">обязательно</span></label>
<input class="form-input" id="new-auth-profile-name" type="text" autocomplete="off" spellcheck="false">
</div>
<div class="form-row" style="grid-template-columns: 1fr 1fr;">
<div class="form-group">
<label class="form-label"><span data-i18n="wizard.step2.profile_kind">Auth kind</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
<label class="form-label"><span data-i18n="wizard.step2.profile_kind">Тип авторизации</span> <span class="form-label-required" data-i18n="workspace_setup.required">обязательно</span></label>
<select class="form-select" id="new-auth-profile-kind" data-testid="wizard-auth-profile-kind-select" onchange="updateAuthProfileCreateUi()">
<option value="bearer" data-i18n="wizard.step2.auth_kind.bearer">Bearer token</option>
<option value="basic" data-i18n="wizard.step2.auth_kind.basic">Basic auth</option>
<option value="api_key_header" data-i18n="wizard.step2.auth_kind.api_key_header">API key header</option>
<option value="api_key_query" data-i18n="wizard.step2.auth_kind.api_key_query">API key query</option>
<option value="bearer" data-i18n="wizard.step2.auth_kind.bearer">Bearer-токен</option>
<option value="basic" data-i18n="wizard.step2.auth_kind.basic">Логин и пароль</option>
<option value="api_key_header" data-i18n="wizard.step2.auth_kind.api_key_header">API-ключ в заголовке</option>
<option value="api_key_query" data-i18n="wizard.step2.auth_kind.api_key_query">API-ключ в параметре запроса</option>
</select>
</div>
<div class="form-group" id="auth-profile-header-name-group">
<label class="form-label" data-i18n="wizard.step2.header_name">Header name</label>
<label class="form-label" data-i18n="wizard.step2.header_name">Имя заголовка</label>
<input class="form-input input-mono" id="new-auth-profile-header-name" type="text" value="Authorization" autocomplete="off" spellcheck="false">
</div>
</div>
<div class="form-group" id="auth-profile-query-param-group" hidden>
<label class="form-label" data-i18n="wizard.step2.query_param">Query param</label>
<label class="form-label" data-i18n="wizard.step2.query_param">Параметр запроса</label>
<input class="form-input input-mono" id="new-auth-profile-query-param" type="text" value="api_key" autocomplete="off" spellcheck="false">
</div>
<div class="form-row" id="auth-profile-basic-secret-row" hidden style="grid-template-columns: 1fr 1fr;">
<div class="form-group">
<label class="form-label" data-i18n="wizard.step2.username_secret">Username secret</label>
<label class="form-label" data-i18n="wizard.step2.username_secret">Секрет логина</label>
<select class="form-select" id="new-auth-profile-username-secret" data-testid="wizard-auth-username-secret-select"></select>
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step2.password_secret">Password secret</label>
<label class="form-label" data-i18n="wizard.step2.password_secret">Секрет пароля</label>
<select class="form-select" id="new-auth-profile-password-secret" data-testid="wizard-auth-password-secret-select"></select>
</div>
</div>
<div class="form-group" id="auth-profile-single-secret-group">
<label class="form-label" data-i18n="wizard.step2.secret_value">Secret</label>
<label class="form-label" data-i18n="wizard.step2.secret_value">Секрет</label>
<select class="form-select" id="new-auth-profile-secret-id" data-testid="wizard-auth-secret-select"></select>
</div>
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
<button class="btn-ghost-sm" data-testid="wizard-open-quick-secret" onclick="openQuickSecretModal(event)" data-i18n="wizard.step2.quick_secret">Quick create secret</button>
<a class="btn-ghost-sm" href="/secrets" target="_blank" rel="noopener" data-i18n="wizard.step2.manage_secrets">Open secrets page</a>
<button class="btn-ghost-sm" data-testid="wizard-open-quick-secret" onclick="openQuickSecretModal(event)" data-i18n="wizard.step2.quick_secret">Быстро создать секрет</button>
<a class="btn-ghost-sm" href="/secrets" target="_blank" rel="noopener" data-i18n="wizard.step2.manage_secrets">Открыть страницу секретов</a>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step2.static_headers">Static headers (optional)</label>
<label class="form-label" data-i18n="wizard.step2.static_headers">Статические заголовки (опционально)</label>
<div class="code-block" style="border-radius: 6px;">
<div class="code-toolbar">
<div class="code-dots"><span></span><span></span><span></span></div>
@@ -165,11 +165,11 @@
<textarea class="form-textarea code-textarea" id="new-upstream-static-headers" rows="5">{
}</textarea>
</div>
<div class="form-hint" data-i18n="wizard.step2.static_headers_hint">Optional non-secret headers sent on every request to this upstream. Use auth profiles for credentials.</div>
<div class="form-hint" data-i18n="wizard.step2.static_headers_hint">Необязательные заголовки без секретных значений. Токены, пароли и ключи храните через профиль авторизации.</div>
</div>
<div style="display:flex; gap:8px; margin-top:4px;">
<button class="btn-primary-sm" onclick="saveNewUpstream(event)" data-i18n="wizard.step2.save_upstream">Save upstream</button>
<button class="btn-ghost-sm" onclick="cancelNewUpstream(event)" data-i18n="btn.cancel">Cancel</button>
<button class="btn-primary-sm" onclick="saveNewUpstream(event)" data-i18n="wizard.step2.save_upstream">Сохранить API-хост</button>
<button class="btn-ghost-sm" onclick="cancelNewUpstream(event)" data-i18n="btn.cancel">Отмена</button>
</div>
</div>
</div>
@@ -186,19 +186,19 @@
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step2.endpoint_title">Endpoint</div>
<div class="config-card-subtitle" data-i18n="wizard.step2.endpoint_subtitle">Path and HTTP method for this specific operation</div>
<div class="config-card-title" data-i18n="wizard.step2.endpoint_title">Путь API</div>
<div class="config-card-subtitle" data-i18n="wizard.step2.endpoint_subtitle">Путь и HTTP-метод для конкретной операции</div>
</div>
<span class="config-card-optional" style="color: var(--error, #f87171);" data-i18n="wizard.required">Required</span>
<span class="config-card-optional" style="color: var(--error, #f87171);" data-i18n="wizard.required">Обязательно</span>
</div>
<div class="config-card-body" style="gap: 16px;">
<div class="form-group">
<label class="form-label">
<span data-i18n="wizard.step2.path_template">Path template</span>
<span class="form-label-required" data-i18n="workspace_setup.required">required</span>
<span data-i18n="wizard.step2.path_template">Шаблон пути</span>
<span class="form-label-required" data-i18n="workspace_setup.required">обязательно</span>
</label>
<input class="form-input input-mono" id="endpoint-path" type="text" value="/v1/leads" autocomplete="off" spellcheck="false">
<div class="form-hint" data-i18n="wizard.step2.path_hint">Appended to the upstream base URL. Use {param} for path variables.</div>
<div class="form-hint" data-i18n="wizard.step2.path_hint">Добавляется к базовому URL API-хоста. Используйте {param} для параметров пути.</div>
</div>
</div>
</div>
+13 -13
View File
@@ -5,8 +5,8 @@
<div class="step-panel-eyebrow-line"></div>
<span data-step-counter>Step 3 of 5</span>
</div>
<h1 class="step-panel-title" data-i18n="wizard.step3.rest.title">HTTP method &amp; format</h1>
<p class="step-panel-subtitle" data-i18n="wizard.step3.rest.subtitle">Choose the HTTP verb this operation sends and configure content negotiation headers. Crank will serialize MCP tool arguments into the appropriate request body format.</p>
<h1 class="step-panel-title" data-i18n="wizard.step3.rest.title">HTTP метод и формат</h1>
<p class="step-panel-subtitle" data-i18n="wizard.step3.rest.subtitle">Выберите HTTP-метод и формат запроса. Crank подставит параметры MCP инструмента в запрос к API.</p>
</div>
<!-- HTTP method picker -->
@@ -18,8 +18,8 @@
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step3.rest.method_title">HTTP method</div>
<div class="config-card-subtitle" data-i18n="wizard.step3.rest.method_subtitle">Verb sent to the upstream on every tool invocation</div>
<div class="config-card-title" data-i18n="wizard.step3.rest.method_title">HTTP метод</div>
<div class="config-card-subtitle" data-i18n="wizard.step3.rest.method_subtitle">Метод, который отправляется в API при каждом вызове инструмента</div>
</div>
<span class="config-card-optional" style="color:var(--error,#f87171);" data-i18n="wizard.required">Required</span>
</div>
@@ -27,23 +27,23 @@
<div class="method-grid">
<button class="method-card" data-method="GET" onclick="selectMethod(this)">
<span class="method-name">GET</span>
<span class="method-desc" data-i18n="wizard.step3.rest.read">Read</span>
<span class="method-desc" data-i18n="wizard.step3.rest.read">Чтение</span>
</button>
<button class="method-card active" data-method="POST" onclick="selectMethod(this)">
<span class="method-name">POST</span>
<span class="method-desc" data-i18n="wizard.step3.rest.create">Create</span>
<span class="method-desc" data-i18n="wizard.step3.rest.create">Создание</span>
</button>
<button class="method-card" data-method="PUT" onclick="selectMethod(this)">
<span class="method-name">PUT</span>
<span class="method-desc" data-i18n="wizard.step3.rest.replace">Replace</span>
<span class="method-desc" data-i18n="wizard.step3.rest.replace">Замена</span>
</button>
<button class="method-card" data-method="PATCH" onclick="selectMethod(this)">
<span class="method-name">PATCH</span>
<span class="method-desc" data-i18n="wizard.step3.rest.update">Update</span>
<span class="method-desc" data-i18n="wizard.step3.rest.update">Обновление</span>
</button>
<button class="method-card" data-method="DELETE" onclick="selectMethod(this)">
<span class="method-name">DELETE</span>
<span class="method-desc" data-i18n="wizard.step3.rest.remove">Remove</span>
<span class="method-desc" data-i18n="wizard.step3.rest.remove">Удаление</span>
</button>
</div>
<div class="method-callout" id="method-callout-rest" hidden></div>
@@ -60,8 +60,8 @@
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step3.rest.format_title">Request format</div>
<div class="config-card-subtitle" data-i18n="wizard.step3.rest.format_subtitle">Content negotiation and serialisation</div>
<div class="config-card-title" data-i18n="wizard.step3.rest.format_title">Формат запроса</div>
<div class="config-card-subtitle" data-i18n="wizard.step3.rest.format_subtitle">Формат данных запроса и ожидаемого ответа</div>
</div>
<span class="config-card-optional" data-i18n="wizard.optional">Optional</span>
</div>
@@ -75,7 +75,7 @@
<option>multipart/form-data</option>
<option>text/plain</option>
</select>
<div class="form-hint" data-i18n="wizard.step3.rest.content_type_hint">How the request body is encoded when sent to the upstream.</div>
<div class="form-hint" data-i18n="wizard.step3.rest.content_type_hint">В каком формате отправлять данные в API.</div>
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step3.rest.accept">Accept</label>
@@ -84,7 +84,7 @@
<option>text/plain</option>
<option>*/*</option>
</select>
<div class="form-hint" data-i18n="wizard.step3.rest.accept_hint">Accepted response content types from the upstream server.</div>
<div class="form-hint" data-i18n="wizard.step3.rest.accept_hint">Какой формат ответа ожидать от API.</div>
</div>
</div>
</div>
+24 -24
View File
@@ -4,8 +4,8 @@
<div class="step-panel-eyebrow-line"></div>
<span data-step-counter>Step 4 of 5</span>
</div>
<h1 class="step-panel-title" data-i18n="wizard.step4.title">Tool config</h1>
<p class="step-panel-subtitle" data-i18n="wizard.step4.subtitle">Name your tool, write the LLM-facing description, and define the input/output schemas. The description is the primary signal the model uses when deciding whether to call this tool.</p>
<h1 class="step-panel-title" data-i18n="wizard.step4.title">Настройки инструмента</h1>
<p class="step-panel-subtitle" data-i18n="wizard.step4.subtitle">Задайте имя инструмента, описание для LLM и определите входную/выходную схему. Описание — главный сигнал, по которому модель решает, вызывать ли этот инструмент.</p>
</div>
<div class="config-card" style="margin-bottom: 20px;">
@@ -16,47 +16,47 @@
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step4.identity_title">Tool identity</div>
<div class="config-card-subtitle" data-i18n="wizard.step4.identity_subtitle">Machine-readable name and LLM-facing description</div>
<div class="config-card-title" data-i18n="wizard.step4.identity_title">Параметры инструмента</div>
<div class="config-card-subtitle" data-i18n="wizard.step4.identity_subtitle">Имя инструмента и описание его назначения</div>
</div>
</div>
<div class="config-card-body" style="gap: 20px;">
<div class="form-group">
<label class="form-label">
<span data-i18n="wizard.step4.tool_name">Tool name</span>
<span class="form-label-required" data-i18n="workspace_setup.required">required</span>
<span data-i18n="wizard.step4.tool_name">Имя инструмента</span>
<span class="form-label-required" data-i18n="workspace_setup.required">обязательно</span>
</label>
<input id="tool-name" class="form-input input-mono" type="text" value="create_crm_lead" autocomplete="off" spellcheck="false">
<div class="form-hint" data-i18n="wizard.step4.tool_name_hint">Machine-readable identifier used in MCP tool calls. Only lowercase letters, numbers and underscores. Cannot be changed after publishing.</div>
<div class="form-hint" data-i18n="wizard.step4.tool_name_hint">Техническое имя для вызова MCP инструмента. Допустимы только строчные английские буквы, цифры и подчеркивания. После публикации изменить нельзя.</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">
<span data-i18n="wizard.step4.display_name">Display name</span>
<span class="form-label-required" data-i18n="workspace_setup.required">required</span>
<span data-i18n="wizard.step4.display_name">Отображаемое имя</span>
<span class="form-label-required" data-i18n="workspace_setup.required">обязательно</span>
</label>
<input id="tool-display-name" class="form-input" type="text" value="Create CRM Lead" autocomplete="off">
<div class="form-hint" data-i18n="wizard.step4.display_name_hint">Human-readable name shown in the console and audit log.</div>
<div class="form-hint" data-i18n="wizard.step4.display_name_hint">Понятное название, которое отображается в интерфейсе и логах.</div>
</div>
<div class="form-group">
<label class="form-label">
<span data-i18n="wizard.step4.tool_title">Tool title</span>
<span class="form-label-optional" data-i18n="workspace_setup.optional">(optional)</span>
<span data-i18n="wizard.step4.tool_title">Короткое описание</span>
<span class="form-label-optional" data-i18n="workspace_setup.optional">(необязательно)</span>
</label>
<input id="tool-title" class="form-input" type="text" value="Create a new CRM lead record" autocomplete="off">
<div class="form-hint" data-i18n="wizard.step4.tool_title_hint">Short imperative sentence shown in the MCP tool manifest.</div>
<div class="form-hint" data-i18n="wizard.step4.tool_title_hint">Короткая фраза, которая будет показана MCP клиенту в описании инструмента.</div>
</div>
</div>
<div class="form-group">
<label class="form-label">
<span data-i18n="wizard.step4.description">Description</span>
<span class="form-label-required" data-i18n="workspace_setup.required">required</span>
<span data-i18n="wizard.step4.description">Описание</span>
<span class="form-label-required" data-i18n="workspace_setup.required">обязательно</span>
</label>
<textarea id="tool-description" class="form-textarea" rows="5">Creates a new lead record in the CRM from the provided contact details. Use this tool when the user wants to add a new prospect or contact. Returns the new lead ID and creation timestamp on success.</textarea>
<div class="form-hint" data-i18n="wizard.step4.description_hint">LLM-facing description. Be precise — this is the primary signal the model uses to decide whether to invoke this tool.</div>
<div class="form-hint" data-i18n="wizard.step4.description_hint">Описание для модели. Пишите точно: по этому тексту модель решает, когда нужно вызвать инструмент.</div>
</div>
</div>
@@ -68,8 +68,8 @@
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title" data-i18n="wizard.step4.description_title">Writing effective descriptions</div>
<div class="info-callout-text" data-i18n="wizard.step4.description_body">Start with a verb ("Creates", "Fetches", "Updates"). Mention key input requirements. Describe what a successful response looks like. Avoid vague phrasing like "handles" or "manages". See description best practices →</div>
<div class="info-callout-title" data-i18n="wizard.step4.description_title">Как писать хорошие описания</div>
<div class="info-callout-text" data-i18n="wizard.step4.description_body">Начинайте с глагола: создает, получает, обновляет. Укажите обязательные входные данные и опишите успешный результат. Избегайте расплывчатых формулировок вроде “обрабатывает” или “управляет”.</div>
</div>
</div>
@@ -82,8 +82,8 @@
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step4.input_schema_title">Input schema</div>
<div class="config-card-subtitle" data-i18n="wizard.step4.input_schema_subtitle">Parameters the LLM passes when calling this tool</div>
<div class="config-card-title" data-i18n="wizard.step4.input_schema_title">Входная схема</div>
<div class="config-card-subtitle" data-i18n="wizard.step4.input_schema_subtitle">Параметры, которые LLM передает при вызове инструмента</div>
</div>
<span class="config-card-optional" data-i18n="wizard.step4.schema_draft">JSON Schema draft-07</span>
</div>
@@ -120,8 +120,8 @@
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step4.output_schema_title">Output schema</div>
<div class="config-card-subtitle" data-i18n="wizard.step4.output_schema_subtitle">Shape of the data returned to the LLM after a successful call</div>
<div class="config-card-title" data-i18n="wizard.step4.output_schema_title">Выходная схема</div>
<div class="config-card-subtitle" data-i18n="wizard.step4.output_schema_subtitle">Форма данных, которые LLM получает после успешного вызова</div>
</div>
<span class="config-card-optional" data-i18n="wizard.step4.schema_draft">JSON Schema draft-07</span>
</div>
@@ -151,8 +151,8 @@
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title" data-i18n="wizard.step4.protocol_agnostic_title">Schemas are protocol-agnostic</div>
<div class="info-callout-text" data-i18n="wizard.step4.protocol_agnostic_body">These schemas describe the MCP contract, not the upstream wire format. Field mapping in Step 5 translates between the two. You can upload a sample JSON response to auto-generate the output schema.</div>
<div class="info-callout-title" data-i18n="wizard.step4.protocol_agnostic_title">Схемы описывают инструмент</div>
<div class="info-callout-text" data-i18n="wizard.step4.protocol_agnostic_body">Входная схема описывает данные, которые MCP клиент передает инструменту. Выходная схема описывает результат работы инструмента. На следующем шаге эти поля связываются с реальным API-запросом и ответом.</div>
</div>
</div>
</div><!-- /step-pane-4 -->
+38 -71
View File
@@ -4,12 +4,12 @@
<div class="step-panel-eyebrow-line"></div>
<span data-step-counter>Step 5 of 5</span>
</div>
<h1 class="step-panel-title" data-i18n="wizard.step5.title">Mapping and execution</h1>
<p class="step-panel-subtitle" data-i18n="wizard.step5.subtitle">Define how MCP tool arguments map to upstream request fields, and how the upstream response maps back to MCP output. Then set execution parameters — auth is configured per-upstream in step 2.</p>
<h1 class="step-panel-title" data-i18n="wizard.step5.title">Связи полей и запуск</h1>
<p class="step-panel-subtitle" data-i18n="wizard.step5.subtitle">Свяжите параметры MCP инструмента с полями API-запроса, а ответ API с результатом инструмента. Авторизация настраивается на шаге 2.</p>
</div>
<div class="section-divider" style="margin-bottom: 16px;">
<span class="section-divider-label" data-i18n="wizard.step5.input_request">Input → Request</span>
<span class="section-divider-label" data-i18n="wizard.step5.input_request">Вход инструмента → API-запрос</span>
<div class="section-divider-line"></div>
</div>
@@ -18,7 +18,7 @@
<div class="code-block" style="border-radius: 0; border: none;">
<div class="code-toolbar">
<div class="code-dots"><span></span><span></span><span></span></div>
<span class="code-toolbar-label">yaml / input-mapping</span>
<span class="code-toolbar-label">yaml / request-fields</span>
</div>
<textarea id="tool-input-mapping" class="form-textarea code-textarea" rows="12">first_name: "$.input.first_name"
last_name: "$.input.last_name"
@@ -31,7 +31,7 @@ source: "$.input.source"</textarea>
</div>
<div class="section-divider" style="margin-bottom: 16px;">
<span class="section-divider-label" data-i18n="wizard.step5.output_response">Response → Output</span>
<span class="section-divider-label" data-i18n="wizard.step5.output_response">Ответ API → результат инструмента</span>
<div class="section-divider-line"></div>
</div>
@@ -40,7 +40,7 @@ source: "$.input.source"</textarea>
<div class="code-block" style="border-radius: 0; border: none;">
<div class="code-toolbar">
<div class="code-dots"><span></span><span></span><span></span></div>
<span class="code-toolbar-label">yaml / output-mapping</span>
<span class="code-toolbar-label">yaml / response-fields</span>
</div>
<textarea id="tool-output-mapping" class="form-textarea code-textarea" rows="10">lead_id: "$.response.data.id"
status: "$.response.data.status"
@@ -51,7 +51,7 @@ owner_id: "$.response.data.owner.id"</textarea>
</div>
<div class="section-divider" style="margin-bottom: 16px;">
<span class="section-divider-label" data-i18n="wizard.step5.execution">Execution</span>
<span class="section-divider-label" data-i18n="wizard.step5.execution">Параметры выполнения</span>
<div class="section-divider-line"></div>
</div>
@@ -64,8 +64,8 @@ owner_id: "$.response.data.owner.id"</textarea>
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step5.exec_title">Execution config</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.exec_subtitle">Timeouts, retry policy and auth profile reference</div>
<div class="config-card-title" data-i18n="wizard.step5.exec_title">Выполнение запроса</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.exec_subtitle">Время ожидания, повторные попытки и профиль авторизации</div>
</div>
<span class="config-card-optional" data-i18n="wizard.optional">Optional</span>
</div>
@@ -73,7 +73,7 @@ owner_id: "$.response.data.owner.id"</textarea>
<div class="code-block" style="border-radius: 0; border: none;">
<div class="code-toolbar">
<div class="code-dots"><span></span><span></span><span></span></div>
<span class="code-toolbar-label">yaml / exec-config</span>
<span class="code-toolbar-label">yaml / execution</span>
</div>
<textarea id="tool-exec-config" class="form-textarea code-textarea" rows="10">timeout_ms: 10000
retry:
@@ -87,19 +87,8 @@ tls:
</div>
</div>
<div class="info-callout" id="wizard-security-level-note" hidden style="margin-bottom: 20px;">
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="8" r="7"/>
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title" data-i18n="wizard.step5.security_level_title">Operation security in Community</div>
<div class="info-callout-text"></div>
</div>
</div>
<div class="section-divider" style="margin-bottom: 16px;">
<span class="section-divider-label" data-i18n="wizard.step5.live_title">Live validation and publishing</span>
<span class="section-divider-label" data-i18n="wizard.step5.live_title">Проверка и публикация</span>
<div class="section-divider-line"></div>
</div>
@@ -109,22 +98,11 @@ tls:
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title" id="wizard-live-status-title" data-i18n="wizard.step5.draft_actions">Draft actions</div>
<div class="info-callout-title" id="wizard-live-status-title" data-i18n="wizard.step5.draft_actions">Результат действия</div>
<div class="info-callout-text" id="wizard-live-status-text"></div>
</div>
</div>
<div class="info-callout info-callout-subtle" style="margin-bottom: 20px;">
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="8" r="7"/>
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title" data-i18n="wizard.step5.live_save_title">Live actions save the draft first</div>
<div class="info-callout-text" data-i18n="wizard.step5.live_save_body">Sample uploads, test runs, YAML actions and publish all persist the current draft before calling the backend.</div>
</div>
</div>
<div class="config-card" style="margin-bottom: 20px;">
<div class="config-card-header">
<div class="config-card-header-icon">
@@ -134,14 +112,14 @@ tls:
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step5.samples_title">Samples and draft generation</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.samples_subtitle">Persist input/output samples, then generate schemas and mappings from real payloads.</div>
<div class="config-card-title" data-i18n="wizard.step5.samples_title">Примеры и автоматическая настройка</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.samples_subtitle">Сохраните пример входных и выходных данных, затем Crank сможет пересобрать схемы и связи полей на их основе.</div>
</div>
</div>
<div class="config-card-body" style="gap: 16px;">
<div class="form-row">
<div class="form-group">
<label class="form-label" data-i18n="wizard.step5.input_sample">Input sample</label>
<label class="form-label" data-i18n="wizard.step5.input_sample">Пример входных данных</label>
<textarea id="wizard-input-sample" class="form-textarea code-textarea" rows="9" spellcheck="false">{
"first_name": "Ada",
"last_name": "Lovelace",
@@ -149,7 +127,7 @@ tls:
}</textarea>
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step5.output_sample">Output sample</label>
<label class="form-label" data-i18n="wizard.step5.output_sample">Пример ответа</label>
<textarea id="wizard-output-sample" class="form-textarea code-textarea" rows="9" spellcheck="false">{
"id": "lead_123",
"status": "created"
@@ -157,9 +135,9 @@ tls:
</div>
</div>
<div style="display:flex; gap: 8px; flex-wrap: wrap;">
<button id="wizard-upload-input-sample" class="btn-primary-sm" type="button" data-i18n="wizard.step5.save_input_sample">Save input sample</button>
<button id="wizard-upload-output-sample" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.save_output_sample">Save output sample</button>
<button id="wizard-generate-draft" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.generate_draft">Generate draft from samples</button>
<button id="wizard-upload-input-sample" class="btn-primary-sm" type="button" data-i18n="wizard.step5.save_input_sample">Сохранить входной пример</button>
<button id="wizard-upload-output-sample" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.save_output_sample">Сохранить выходной пример</button>
<button id="wizard-generate-draft" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.generate_draft">Пересобрать по примерам</button>
</div>
</div>
</div>
@@ -172,13 +150,13 @@ tls:
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step5.test_title">Test run</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.test_subtitle">Run the current draft against the upstream before publishing it.</div>
<div class="config-card-title" data-i18n="wizard.step5.test_title">Тестирование операции</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.test_subtitle">Выполните инструмент на реальном API перед публикацией.</div>
</div>
</div>
<div class="config-card-body" style="gap: 16px;">
<div class="form-group">
<label class="form-label" data-i18n="wizard.step5.test_input_payload">Test input payload</label>
<label class="form-label" data-i18n="wizard.step5.test_input_payload">Входные данные для теста</label>
<textarea id="wizard-test-input" class="form-textarea code-textarea" rows="8" spellcheck="false">{
"first_name": "Ada",
"last_name": "Lovelace",
@@ -186,21 +164,21 @@ tls:
}</textarea>
</div>
<div style="display:flex; gap: 8px; flex-wrap: wrap;">
<button id="wizard-run-test" class="btn-primary-sm" type="button" data-i18n="wizard.step5.run_test">Run test</button>
<button id="wizard-copy-test-response" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.use_response_output">Use response as output sample</button>
<button id="wizard-run-test" class="btn-primary-sm" type="button" data-i18n="wizard.step5.run_test">Запустить тест</button>
<button id="wizard-copy-test-response" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.use_response_output">Использовать ответ как выходной пример</button>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" data-i18n="wizard.step5.request_preview">Request preview</label>
<label class="form-label" data-i18n="wizard.step5.request_preview">Предпросмотр API-запроса</label>
<textarea id="wizard-test-request-preview" class="form-textarea code-textarea" rows="8" spellcheck="false" readonly></textarea>
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step5.response_preview">Response preview</label>
<label class="form-label" data-i18n="wizard.step5.response_preview">Предпросмотр ответа API</label>
<textarea id="wizard-test-response-preview" class="form-textarea code-textarea" rows="8" spellcheck="false" readonly></textarea>
</div>
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step5.errors">Errors</label>
<label class="form-label" data-i18n="wizard.step5.errors">Ошибки</label>
<textarea id="wizard-test-errors" class="form-textarea code-textarea" rows="4" spellcheck="false" readonly></textarea>
</div>
</div>
@@ -214,20 +192,20 @@ tls:
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step5.yaml_title">YAML import and export</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.yaml_subtitle">Move operation drafts between environments without leaving the wizard.</div>
<div class="config-card-title" data-i18n="wizard.step5.yaml_title">Импорт и экспорт YAML</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.yaml_subtitle">Экспортируйте операцию в файл или импортируйте подготовленную YAML-конфигурацию.</div>
</div>
</div>
<div class="config-card-body" style="gap: 16px;">
<div style="display:flex; gap: 8px; flex-wrap: wrap;">
<button id="wizard-export-yaml" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.export_yaml">Export YAML</button>
<button id="wizard-import-yaml-file-trigger" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.load_yaml_file">Load YAML file</button>
<button id="wizard-import-yaml" class="btn-primary-sm" type="button" data-i18n="wizard.step5.import_yaml">Import YAML</button>
<button id="wizard-export-yaml" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.export_yaml">Экспорт YAML</button>
<button id="wizard-import-yaml-file-trigger" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.load_yaml_file">Загрузить YAML-файл</button>
<button id="wizard-import-yaml" class="btn-primary-sm" type="button" data-i18n="wizard.step5.import_yaml">Импорт YAML</button>
<input id="wizard-import-yaml-file" type="file" accept=".yaml,.yml,text/yaml" style="display:none">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step5.yaml_document">YAML document</label>
<textarea id="wizard-import-yaml-text" class="form-textarea code-textarea" rows="10" spellcheck="false" data-i18n-ph="wizard.step5.yaml_placeholder" placeholder="Paste exported YAML here to create or upsert an operation."></textarea>
<label class="form-label" data-i18n="wizard.step5.yaml_document">YAML-конфигурация</label>
<textarea id="wizard-import-yaml-text" class="form-textarea code-textarea" rows="10" spellcheck="false" data-i18n-ph="wizard.step5.yaml_placeholder" placeholder="Вставьте YAML сюда, чтобы создать или обновить операцию."></textarea>
</div>
</div>
</div>
@@ -240,26 +218,15 @@ tls:
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step5.publish_title">Publish</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.publish_subtitle">Save the current draft and expose it to published agents.</div>
<div class="config-card-title" data-i18n="wizard.step5.publish_title">Публикация</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.publish_subtitle">Опубликуйте операцию, чтобы ее можно было добавить агентам.</div>
</div>
</div>
<div class="config-card-body" style="gap: 12px;">
<div class="form-hint" data-i18n="wizard.step5.publish_hint">Publishing uses the current draft version from this wizard session.</div>
<div class="form-hint" data-i18n="wizard.step5.publish_hint">Перед публикацией Crank сохраняет текущие настройки из мастера.</div>
<div style="display:flex; gap: 8px; flex-wrap: wrap;">
<button id="wizard-publish-operation" class="btn-primary-sm" type="button" data-i18n="wizard.step5.publish_action">Publish operation</button>
<button id="wizard-publish-operation" class="btn-primary-sm" type="button" data-i18n="wizard.step5.publish_action">Опубликовать операцию</button>
</div>
</div>
</div>
<div class="info-callout" style="margin-bottom: 24px;">
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="8" r="7"/>
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title" data-i18n="wizard.step5.editable_title">Operation stays editable in this wizard</div>
<div class="info-callout-text" data-i18n="wizard.step5.editable_body">Saving creates or updates the current draft in place. Use the live actions above to upload samples, test the draft, export or import YAML, and publish when the upstream contract is ready.</div>
</div>
</div>
</div><!-- /step-pane-5 -->
+1 -1
View File
@@ -75,7 +75,7 @@
<div class="form-group">
<label class="form-label"><span data-i18n="workspace_setup.description">Description</span> <span class="form-label-optional" data-i18n="workspace_setup.optional">(optional)</span></label>
<textarea class="form-textarea" id="ws-desc" rows="2" data-i18n-ph="workspace_setup.description_placeholder" placeholder="What does this workspace do?"></textarea>
<textarea class="form-textarea" id="ws-desc" rows="2" data-i18n-ph="workspace_setup.description_placeholder" placeholder="What tasks does this workspace perform?"></textarea>
</div>
</div>
+1 -35
View File
@@ -44,10 +44,6 @@
</button>
<button class="nav-icon-btn" title="Notifications">
<svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg>
</button>
<div class="nav-divider"></div>
<!-- User menu -->
@@ -90,7 +86,7 @@
<div class="page-header">
<div>
<h1 class="page-heading" data-i18n="ops.title">Operations</h1>
<p class="page-subheading" data-i18n="ops.subtitle">Tool contracts that expose your upstream APIs as MCP tools</p>
<p class="page-subheading" data-i18n="ops.subtitle">Catalog of tools. List of created MCP tools for API endpoints.</p>
</div>
<button class="btn-new" @click="handleNewOperation()">
<svg width="13" height="13"><use href="icons/general/plus.svg#icon"/></svg>
@@ -151,36 +147,6 @@
>
</div>
<!-- Protocol dropdown -->
<div class="filter-dropdown" @click.stop>
<button
class="filter-chip"
:class="{ 'has-value': filterProtocol, open: openDropdown === 'protocol' }"
@click="toggleDropdown('protocol')"
>
<span x-text="filterProtocol ? filterProtocol.toUpperCase() : tKey('filter.protocol')"></span>
<svg class="chevron" width="10" height="10"><use href="icons/general/chevron-down.svg#icon"/></svg>
</button>
<div class="dropdown-menu" x-show="openDropdown === 'protocol'" x-transition>
<template x-for="p in ['rest']" :key="p">
<button
class="dropdown-item"
:class="{ selected: filterProtocol === p }"
@click="setProtocol(p)"
>
<span x-text="p === 'rest' ? 'REST' : p.toUpperCase()"></span>
<svg class="check" width="12" height="12"><use href="icons/general/check.svg#icon"/></svg>
</button>
</template>
<template x-if="filterProtocol">
<div>
<div class="dropdown-divider"></div>
<button class="dropdown-clear" @click="clearProtocol()" data-i18n="filter.clear">Clear filter</button>
</div>
</template>
</div>
</div>
<!-- Category dropdown -->
<div class="filter-dropdown" @click.stop>
<button
-48
View File
@@ -1,6 +1,5 @@
var KEYS = [];
var AGENTS = [];
var capabilities = null;
var currentWorkspaceId = null;
var currentAgentId = null;
var selectedScopes = new Set(['read']);
@@ -21,10 +20,6 @@ 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));
@@ -114,47 +109,6 @@ async function loadKeys() {
}
}
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;
@@ -611,10 +565,8 @@ function copyPrefix(prefix) {
document.addEventListener('DOMContentLoaded', async function() {
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
await loadCapabilities();
await loadKeys();
window.addEventListener('crank:workspacechange', function() {
loadCapabilities();
loadKeys();
});
});
+3
View File
@@ -298,6 +298,9 @@
listAuthProfiles: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/auth-profiles');
},
createAuthProfile: function(workspaceId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/auth-profiles', payload);
},
listUpstreams: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/upstreams');
},
+1 -22
View File
@@ -99,7 +99,6 @@ document.addEventListener('alpine:init', function() {
pageSize: PAGE_SIZE_DESKTOP,
search: '',
tab: 'all',
filterProtocol: null,
filterCategory: null,
filterAgent: null,
sort: 'created_desc',
@@ -201,10 +200,6 @@ document.addEventListener('alpine:init', function() {
});
}
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);
}
@@ -222,7 +217,6 @@ document.addEventListener('alpine:init', function() {
var key = [
this._opsVersion,
this.search.trim(),
this.filterProtocol || '',
this.filterCategory || '',
this.filterAgent || '',
].join('|');
@@ -315,7 +309,6 @@ document.addEventListener('alpine:init', function() {
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);
@@ -325,18 +318,16 @@ document.addEventListener('alpine:init', function() {
},
get hasActiveFilters() {
return !!(this.filterProtocol || this.filterCategory || this.filterAgent || this.search.trim());
return !!(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 = '';
@@ -392,12 +383,6 @@ document.addEventListener('alpine:init', function() {
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;
@@ -415,12 +400,6 @@ document.addEventListener('alpine:init', function() {
this.page = 1;
},
clearProtocol() {
this.filterProtocol = null;
this.openDropdown = null;
this.page = 1;
},
clearCategory() {
this.filterCategory = null;
this.openDropdown = null;
+320 -392
View File
File diff suppressed because it is too large Load Diff
-42
View File
@@ -85,47 +85,6 @@ function populateCurrentSession(session) {
});
}
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;
@@ -166,7 +125,6 @@ async function loadCapabilities() {
try {
settingsCapabilities = await window.CrankApi.getCapabilities();
populateCapabilities(settingsCapabilities);
} catch (_error) {
}
}
+20
View File
@@ -104,6 +104,15 @@ async function loadWizardAuthResources() {
renderAuthProfileOptions();
}
async function refreshWizardAuthResources() {
var previousUpstream = selectedUpstreamId;
await loadWizardAuthResources();
renderDropdownList('');
if (previousUpstream && upstreams.some(function(item) { return item.id === previousUpstream; })) {
pickUpstream(previousUpstream);
}
}
function renderAuthProfileOptions() {
var profileSelect = document.getElementById('new-upstream-auth-profile');
var secretSelect = document.getElementById('new-auth-profile-secret-id');
@@ -186,6 +195,17 @@ function updateUpstreamAuthUi() {
if (existingGroup) existingGroup.hidden = mode !== 'existing';
if (createGroup) createGroup.hidden = mode !== 'create';
updateAuthProfileCreateUi();
if (mode === 'existing' || mode === 'create') {
void refreshWizardAuthResources();
}
}
function refreshWizardAuthResourcesOnFocus() {
var mode = textValue('new-upstream-auth-mode') || 'none';
var form = document.getElementById('upstream-new-form');
if (form && !form.hidden && (mode === 'existing' || mode === 'create')) {
void refreshWizardAuthResources();
}
}
function buildUpstreamTriggerContent(name, url) {
+4 -3
View File
@@ -101,6 +101,7 @@ async function initWizardPage() {
bindWizardLiveActions();
updateUpstreamAuthUi();
renderEditionCapabilityHints(editionCapabilities);
window.addEventListener('focus', refreshWizardAuthResourcesOnFocus);
var quickSecretModal = document.getElementById('quick-secret-modal');
var quickSecretClose = document.getElementById('quick-secret-close-btn');
@@ -178,9 +179,9 @@ if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'fun
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 включайте только те поля, которые хотите изменить.' } } },
POST: { icon: 'info', text: { en: { title: 'POST selected — request body is required.', body: 'In step 4 you will define the JSON schema for the request body. Crank will convert MCP tool parameters into the selected format.' }, ru: { title: 'Выбран POST — требуется тело запроса.', body: 'На шаге 4 вы зададите JSON-схему тела запроса. 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 request schema in step 4.' }, ru: { title: 'Выбран PUT — полная замена ресурса.', body: 'Тело запроса должно содержать полное представление ресурса. Задайте схему запроса на шаге 4.' } } },
PATCH: { icon: 'info', text: { en: { title: 'PATCH selected — partial update.', body: 'Only include the fields you want to modify in the request schema defined in step 4.' }, ru: { title: 'Выбран PATCH — частичное обновление.', body: 'В схему запроса на шаге 4 включайте только те поля, которые хотите изменить.' } } },
};
function buildWizardIconSvg(href, width, height) {
+2 -3
View File
@@ -16,9 +16,8 @@ test('workspace and settings pages show live session data', async ({ page }) =>
await expect(page.locator('.page-title')).toHaveText(localized('Account settings', 'Настройки аккаунта'));
await expect(page.locator('#settings-session-summary')).toBeVisible();
await expect(page.locator('#section-security')).toBeVisible();
await expect(page.locator('#settings-security-capability-title')).toContainText(localized('Community build', 'Редакция Community'));
await expect(page.locator('#settings-security-capability-body')).toContainText(localized('static agent keys', 'статические ключи агентов'));
await expect(page.locator('#settings-security-capability-title')).toHaveCount(0);
await expect(page.locator('#settings-security-capability-body')).toHaveCount(0);
await expect(page.locator('#settings-ws-protocol')).toHaveCount(0);
await expect(page.locator('#settings-ws-timezone')).toHaveCount(0);
await expect(page.locator('#settings-notifications-capability-title')).toContainText(localized('Community build boundary', 'Граница редакции Community'));
});