feat: add streaming ui configuration

This commit is contained in:
a.tolmachev
2026-04-06 12:04:49 +03:00
parent fdd0a45124
commit b40daf4f54
20 changed files with 2254 additions and 20 deletions
+171
View File
@@ -1550,3 +1550,174 @@
width: auto;
}
}
.list-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.list-toolbar-group {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.page-select {
min-height: 36px;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg-overlay);
color: var(--text-secondary);
font-size: 13px;
font-family: 'Inter', sans-serif;
}
.resource-list {
display: grid;
gap: 14px;
}
.resource-card {
background: var(--bg-overlay);
border: 1px solid var(--border);
border-radius: 14px;
padding: 16px;
}
.resource-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
margin-bottom: 12px;
}
.resource-card-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.resource-card-subtitle {
margin-top: 4px;
font-size: 12px;
color: var(--text-muted);
word-break: break-all;
}
.resource-card-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.resource-meta-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 14px;
}
.resource-meta-item {
min-width: 0;
}
.resource-meta-label {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 4px;
}
.resource-meta-value {
font-size: 13px;
color: var(--text-primary);
line-height: 1.5;
word-break: break-word;
}
.resource-detail-block {
margin-top: 12px;
border-top: 1px solid var(--border-subtle);
padding-top: 12px;
}
.resource-detail-title {
font-size: 12px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
}
.resource-detail-pre {
margin: 0;
padding: 12px 14px;
border-radius: 12px;
background: rgba(12, 17, 24, 0.8);
border: 1px solid var(--border-subtle);
color: var(--text-secondary);
font-size: 12px;
line-height: 1.5;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
.resource-pill-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-top: 8px;
}
.resource-status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 9px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.04);
color: var(--text-secondary);
}
.resource-status-pill.active,
.resource-status-pill.running,
.resource-status-pill.succeeded {
border-color: rgba(63, 185, 80, 0.28);
background: rgba(63, 185, 80, 0.12);
color: #71dd8a;
}
.resource-status-pill.idle,
.resource-status-pill.pending {
border-color: rgba(110, 118, 129, 0.28);
background: rgba(110, 118, 129, 0.12);
color: var(--text-secondary);
}
.resource-status-pill.failed,
.resource-status-pill.cancelled,
.resource-status-pill.stopped {
border-color: rgba(248, 81, 73, 0.28);
background: rgba(248, 81, 73, 0.12);
color: #ff8a83;
}
.resource-status-pill.completed {
border-color: rgba(56, 189, 248, 0.28);
background: rgba(56, 189, 248, 0.12);
color: #79d8ff;
}
+32
View File
@@ -137,6 +137,38 @@
align-items: flex-start;
}
.checkbox-pill {
display: inline-flex;
align-items: center;
gap: 10px;
min-height: 38px;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg-overlay);
color: var(--text-secondary);
font-size: 13px;
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}
.checkbox-pill:hover {
border-color: var(--bg-muted);
color: var(--text-primary);
}
.checkbox-pill input {
width: 15px;
height: 15px;
accent-color: var(--accent);
}
.checkbox-pill:has(input:checked) {
border-color: rgba(13, 148, 136, 0.35);
background: rgba(13, 148, 136, 0.12);
color: var(--text-primary);
}
/* ══════════════════════════════════════════════════
STEP SIDEBAR
══════════════════════════════════════════════════ */
+112
View File
@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="../">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crank — Async Jobs</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/workspace.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
</head>
<body>
<nav class="navbar">
<a href="/" class="nav-logo">
<div class="nav-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
<span class="nav-logo-text">Crank</span>
</a>
<div class="ws-switcher" id="ws-switcher">
<button class="ws-switcher-trigger" onclick="toggleWsSwitcher(event)">
<div class="ws-dot" id="ws-dot">A</div>
<span class="ws-current-name" id="ws-current-name">workspace</span>
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="ws-dropdown" id="ws-dropdown" style="display:none">
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
<div id="ws-dropdown-list"></div>
<div class="ws-dropdown-footer">
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
<svg width="12" height="12" 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="nav.create_workspace">Create workspace</span>
</button>
</div>
</div>
</div>
<div class="nav-links">
<a href="/" class="nav-link" data-i18n="nav.operations">Operations</a>
<a href="/agents" class="nav-link" data-i18n="nav.agents">Agents</a>
<a href="/api-keys" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="/logs" class="nav-link" data-i18n="nav.logs">Logs</a>
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
</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>
<div class="nav-kbd"><svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg><span>⌘K</span></div>
<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>
<div class="user-dropdown">
<div class="user-dropdown-header"><div class="user-dropdown-name">Crank</div><div class="user-dropdown-role"></div></div>
<button class="user-dropdown-item" data-action="settings-link"><svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg><span data-i18n="nav.settings">Settings</span></button>
<div class="dropdown-divider"></div>
<button class="user-dropdown-item danger" data-action="logout"><svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg><span data-i18n="nav.logout">Log out</span></button>
</div>
</div>
</div>
</nav>
<div class="mobile-nav">
<a href="/" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
<a href="/agents" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
<a href="/api-keys" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="/logs" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
<a href="/usage" class="mobile-nav-link" data-i18n="nav.usage">Usage</a>
</div>
<div class="page">
<div class="page-header">
<div class="page-header-text">
<h1 class="page-title" data-i18n="async_jobs.title">Async jobs</h1>
<p class="page-subtitle" data-i18n="async_jobs.subtitle">Inspect deferred jobs started by streaming MCP tool families.</p>
</div>
<div class="page-header-actions">
<button class="btn-secondary" id="async-jobs-refresh" type="button" data-i18n="async_jobs.refresh">Refresh</button>
</div>
</div>
<div class="section-card">
<div class="section-card-body">
<div class="list-toolbar">
<div class="list-toolbar-group">
<select class="page-select" id="async-jobs-status">
<option value="" data-i18n="async_jobs.filter.all_statuses">All statuses</option>
<option value="created" data-i18n="async_jobs.status.created">Created</option>
<option value="running" data-i18n="async_jobs.status.running">Running</option>
<option value="completed" data-i18n="async_jobs.status.completed">Completed</option>
<option value="failed" data-i18n="async_jobs.status.failed">Failed</option>
<option value="cancelled" data-i18n="async_jobs.status.cancelled">Cancelled</option>
<option value="expired" data-i18n="async_jobs.status.expired">Expired</option>
</select>
</div>
<div class="list-toolbar-group">
<span class="section-card-subtitle" id="async-jobs-summary"></span>
</div>
</div>
<div class="resource-list" id="async-jobs-list"></div>
</div>
</div>
</div>
<script src="js/nav.js"></script>
<script src="js/async-jobs.js"></script>
</body>
</html>
+117
View File
@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="../">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crank — Stream Sessions</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/workspace.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
</head>
<body>
<nav class="navbar">
<a href="/" class="nav-logo">
<div class="nav-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
<span class="nav-logo-text">Crank</span>
</a>
<div class="ws-switcher" id="ws-switcher">
<button class="ws-switcher-trigger" onclick="toggleWsSwitcher(event)">
<div class="ws-dot" id="ws-dot">A</div>
<span class="ws-current-name" id="ws-current-name">workspace</span>
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="ws-dropdown" id="ws-dropdown" style="display:none">
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
<div id="ws-dropdown-list"></div>
<div class="ws-dropdown-footer">
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
<svg width="12" height="12" 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="nav.create_workspace">Create workspace</span>
</button>
</div>
</div>
</div>
<div class="nav-links">
<a href="/" class="nav-link" data-i18n="nav.operations">Operations</a>
<a href="/agents" class="nav-link" data-i18n="nav.agents">Agents</a>
<a href="/api-keys" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="/logs" class="nav-link" data-i18n="nav.logs">Logs</a>
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
</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>
<div class="nav-kbd"><svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg><span>⌘K</span></div>
<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>
<div class="user-dropdown">
<div class="user-dropdown-header"><div class="user-dropdown-name">Crank</div><div class="user-dropdown-role"></div></div>
<button class="user-dropdown-item" data-action="settings-link"><svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg><span data-i18n="nav.settings">Settings</span></button>
<div class="dropdown-divider"></div>
<button class="user-dropdown-item danger" data-action="logout"><svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg><span data-i18n="nav.logout">Log out</span></button>
</div>
</div>
</div>
</nav>
<div class="mobile-nav">
<a href="/" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
<a href="/agents" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
<a href="/api-keys" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="/logs" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
<a href="/usage" class="mobile-nav-link" data-i18n="nav.usage">Usage</a>
</div>
<div class="page">
<div class="page-header">
<div class="page-header-text">
<h1 class="page-title" data-i18n="stream_sessions.title">Stream sessions</h1>
<p class="page-subtitle" data-i18n="stream_sessions.subtitle">Inspect stateful streaming sessions created by generated MCP tool families.</p>
</div>
<div class="page-header-actions">
<button class="btn-secondary" id="stream-sessions-refresh" type="button" data-i18n="stream_sessions.refresh">Refresh</button>
</div>
</div>
<div class="section-card">
<div class="section-card-body">
<div class="list-toolbar">
<div class="list-toolbar-group">
<select class="page-select" id="stream-sessions-status">
<option value="" data-i18n="stream_sessions.filter.all_statuses">All statuses</option>
<option value="created" data-i18n="stream_sessions.status.created">Created</option>
<option value="running" data-i18n="stream_sessions.status.running">Running</option>
<option value="failed" data-i18n="stream_sessions.status.failed">Failed</option>
<option value="stopped" data-i18n="stream_sessions.status.stopped">Stopped</option>
<option value="expired" data-i18n="stream_sessions.status.expired">Expired</option>
</select>
<select class="page-select" id="stream-sessions-mode">
<option value="" data-i18n="stream_sessions.filter.all_modes">All modes</option>
<option value="window">window</option>
<option value="session">session</option>
<option value="async_job">async_job</option>
</select>
</div>
<div class="list-toolbar-group">
<span class="section-card-subtitle" id="stream-sessions-summary"></span>
</div>
</div>
<div class="resource-list" id="stream-sessions-list"></div>
</div>
</div>
</div>
<script src="js/nav.js"></script>
<script src="js/stream-sessions.js"></script>
</body>
</html>
+2
View File
@@ -235,6 +235,8 @@
</div><!-- /wizard-shell -->
<script src="../../js/streaming-form.js"></script>
<script src="../../js/stream-test-run.js"></script>
<script src="../../js/wizard.js"></script>
<script src="../../js/nav.js"></script>
+158
View File
@@ -87,6 +87,164 @@ tls:
</div>
</div>
<div id="wizard-streaming-config-card" class="config-card" style="margin-bottom: 20px;">
<div class="config-card-header">
<div class="config-card-header-icon">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 4h10M3 8h10M3 12h6"/>
<circle cx="12" cy="12" r="1.5"/>
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.streaming.title">Streaming execution</div>
<div class="config-card-subtitle" data-i18n="wizard.streaming.subtitle">Bounded stream, session and async job settings for MCP tool families.</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.streaming.execution_mode">Execution mode</label>
<select id="streaming-mode" class="form-select">
<option value="unary">Unary</option>
</select>
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.transport_behavior">Transport behavior</label>
<select id="streaming-transport-behavior" class="form-select">
<option value="request_response">Request / response</option>
</select>
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.aggregation_mode">Aggregation mode</label>
<select id="streaming-aggregation-mode" class="form-select">
<option value="raw_items" data-i18n="wizard.streaming.aggregation.raw_items">Raw items</option>
<option value="summary_only" data-i18n="wizard.streaming.aggregation.summary_only">Summary only</option>
<option value="summary_plus_samples" data-i18n="wizard.streaming.aggregation.summary_plus_samples">Summary + samples</option>
<option value="stats" data-i18n="wizard.streaming.aggregation.stats">Stats</option>
<option value="latest_state" data-i18n="wizard.streaming.aggregation.latest_state">Latest state</option>
</select>
</div>
</div>
<div id="streaming-mode-help" class="form-hint" data-i18n="wizard.streaming.help.unary">Unary keeps the existing request-response model. Other modes create bounded stream-aware MCP tools.</div>
<div id="streaming-limits-grid" class="form-row">
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.window_duration">Window duration (ms)</label>
<input id="streaming-window-duration-ms" class="form-input" type="number" min="1" step="1" placeholder="5000">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.poll_interval">Poll interval (ms)</label>
<input id="streaming-poll-interval-ms" class="form-input" type="number" min="1" step="1" placeholder="2000">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.upstream_timeout">Upstream timeout (ms)</label>
<input id="streaming-upstream-timeout-ms" class="form-input" type="number" min="1" step="1" placeholder="10000">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.idle_timeout">Idle timeout (ms)</label>
<input id="streaming-idle-timeout-ms" class="form-input" type="number" min="1" step="1" placeholder="30000">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.session_lifetime">Session lifetime (ms)</label>
<input id="streaming-session-lifetime-ms" class="form-input" type="number" min="1" step="1" placeholder="300000">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.max_items">Max items</label>
<input id="streaming-max-items" class="form-input" type="number" min="1" step="1" placeholder="100">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.max_bytes">Max bytes</label>
<input id="streaming-max-bytes" class="form-input" type="number" min="1" step="1" placeholder="65536">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.max_field_length">Max field length</label>
<input id="streaming-max-field-length" class="form-input" type="number" min="1" step="1" placeholder="512">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.sampling_rate">Sampling rate</label>
<input id="streaming-sampling-rate" class="form-input" type="number" min="0" max="1" step="0.1" placeholder="1.0">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.items_path">Items path</label>
<input id="streaming-items-path" class="form-input input-mono" type="text" placeholder="$.items">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.summary_path">Summary path</label>
<input id="streaming-summary-path" class="form-input input-mono" type="text" placeholder="$.summary">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.done_path">Done path</label>
<input id="streaming-done-path" class="form-input input-mono" type="text" placeholder="$.done">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.cursor_path">Cursor path</label>
<input id="streaming-cursor-path" class="form-input input-mono" type="text" placeholder="$.cursor">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.status_path">Status path</label>
<input id="streaming-status-path" class="form-input input-mono" type="text" placeholder="$.status">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.redacted_paths">Redacted paths</label>
<textarea id="streaming-redacted-paths" class="form-textarea code-textarea" rows="4" spellcheck="false" placeholder="$.items[*].token&#10;$.summary.secret"></textarea>
</div>
</div>
<div class="form-row">
<label class="checkbox-pill"><input id="streaming-truncate-item-fields" type="checkbox"> <span data-i18n="wizard.streaming.truncate_item_fields">Truncate item fields</span></label>
<label class="checkbox-pill"><input id="streaming-drop-duplicates" type="checkbox"> <span data-i18n="wizard.streaming.drop_duplicates">Drop duplicates</span></label>
</div>
<div id="streaming-tool-family-block" style="display:none;">
<div class="section-divider" style="margin: 8px 0 16px;">
<span class="section-divider-label" data-i18n="wizard.streaming.tool_family">Tool family</span>
<div class="section-divider-line"></div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.start_tool_name">Start tool name</label>
<input id="streaming-start-tool-name" class="form-input input-mono" type="text" placeholder="logs_follow_start">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.poll_tool_name">Poll tool name</label>
<input id="streaming-poll-tool-name" class="form-input input-mono" type="text" placeholder="logs_follow_poll">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.stop_tool_name">Stop tool name</label>
<input id="streaming-stop-tool-name" class="form-input input-mono" type="text" placeholder="logs_follow_stop">
</div>
</div>
<div class="form-row" id="streaming-async-tool-family-row" style="display:none;">
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.status_tool_name">Status tool name</label>
<input id="streaming-status-tool-name" class="form-input input-mono" type="text" placeholder="deploy_status">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.result_tool_name">Result tool name</label>
<input id="streaming-result-tool-name" class="form-input input-mono" type="text" placeholder="deploy_result">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.streaming.cancel_tool_name">Cancel tool name</label>
<input id="streaming-cancel-tool-name" class="form-input input-mono" type="text" placeholder="deploy_cancel">
</div>
</div>
</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>
<div class="section-divider-line"></div>
+24
View File
@@ -332,6 +332,30 @@
getUsageOverview: function(workspaceId, params) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/usage' + query(params));
},
getProtocolCapabilities: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/protocol-capabilities');
},
listStreamSessions: function(workspaceId, params) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/stream-sessions' + query(params));
},
getStreamSession: function(workspaceId, sessionId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/stream-sessions/' + encodeURIComponent(sessionId));
},
stopStreamSession: function(workspaceId, sessionId) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/stream-sessions/' + encodeURIComponent(sessionId) + '/stop', {});
},
listAsyncJobs: function(workspaceId, params) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/async-jobs' + query(params));
},
getAsyncJob: function(workspaceId, jobId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/async-jobs/' + encodeURIComponent(jobId));
},
cancelAsyncJob: function(workspaceId, jobId) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/async-jobs/' + encodeURIComponent(jobId) + '/cancel', {});
},
getAsyncJobResult: function(workspaceId, jobId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/async-jobs/' + encodeURIComponent(jobId) + '/result');
},
getOperationUsage: function(workspaceId, operationId, params) {
return get(
'/workspaces/' + encodeURIComponent(workspaceId) + '/usage/operations/' + encodeURIComponent(operationId) + query(params)
+181
View File
@@ -0,0 +1,181 @@
document.addEventListener('DOMContentLoaded', function () {
var state = {
items: [],
workspaceId: null,
loading: false,
error: '',
openId: null,
};
var list = document.getElementById('async-jobs-list');
var summary = document.getElementById('async-jobs-summary');
var refreshButton = document.getElementById('async-jobs-refresh');
var statusFilter = document.getElementById('async-jobs-status');
function tKey(key, vars) {
if (!window.t) return key;
return t(key, vars);
}
function currentWorkspaceId() {
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
return workspace ? workspace.id : null;
}
function formatJson(value) {
if (value === null || value === undefined) return '';
if (typeof value === 'string') return value;
return JSON.stringify(value, null, 2);
}
function formatDateTime(value) {
if (!value) return '—';
return new Date(value).toLocaleString();
}
function renderEmpty(title, body) {
list.innerHTML = '<div class="empty-state"><div class="empty-state-title">' + title + '</div><div class="empty-state-text">' + body + '</div></div>';
}
function render() {
summary.textContent = tKey('async_jobs.summary', { count: state.items.length });
if (state.loading && state.items.length === 0) {
renderEmpty(tKey('async_jobs.loading.title'), tKey('async_jobs.loading.body'));
return;
}
if (state.error) {
renderEmpty(tKey('async_jobs.error.title'), state.error);
return;
}
if (!state.items.length) {
renderEmpty(tKey('async_jobs.empty.title'), tKey('async_jobs.empty.body'));
return;
}
list.innerHTML = state.items.map(function(item) {
var open = state.openId === item.id;
var statusLabel = tKey('async_jobs.status.' + item.status);
return [
'<div class="resource-card" data-job-id="' + item.id + '">',
' <div class="resource-card-header">',
' <div>',
' <div class="resource-card-title">' + item.id + '</div>',
' <div class="resource-card-subtitle">' + tKey('async_jobs.operation', { operation: item.operation_id }) + '</div>',
' <div class="resource-pill-row">',
' <span class="resource-status-pill ' + String(item.status || '').toLowerCase() + '">' + statusLabel + '</span>',
' </div>',
' </div>',
' <div class="resource-card-actions">',
item.status === 'created' || item.status === 'running'
? ' <button class="btn-secondary" data-action="cancel" data-job-id="' + item.id + '">' + tKey('async_jobs.cancel') + '</button>'
: '',
' <button class="btn-secondary" data-action="toggle" data-job-id="' + item.id + '">' + (open ? tKey('async_jobs.hide_details') : tKey('async_jobs.show_details')) + '</button>',
' </div>',
' </div>',
' <div class="resource-meta-grid">',
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('async_jobs.meta.created') + '</div><div class="resource-meta-value">' + formatDateTime(item.created_at) + '</div></div>',
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('async_jobs.meta.updated') + '</div><div class="resource-meta-value">' + formatDateTime(item.updated_at) + '</div></div>',
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('async_jobs.meta.finished') + '</div><div class="resource-meta-value">' + formatDateTime(item.finished_at) + '</div></div>',
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('async_jobs.meta.expires') + '</div><div class="resource-meta-value">' + formatDateTime(item.expires_at) + '</div></div>',
' </div>',
open
? ' <div class="resource-detail-block"><div class="resource-detail-title">' + tKey('async_jobs.progress') + '</div><pre class="resource-detail-pre">' + formatJson(item.progress) + '</pre></div>'
: '',
open
? ' <div class="resource-detail-block"><div class="resource-detail-title">' + tKey('async_jobs.error_preview') + '</div><pre class="resource-detail-pre">' + formatJson(item.error) + '</pre></div>'
: '',
open && (item.status === 'completed' || item.status === 'failed' || item.status === 'cancelled' || item.status === 'expired')
? ' <div class="resource-detail-block"><div class="resource-detail-title">' + tKey('async_jobs.result') + '</div><pre class="resource-detail-pre" data-result-for="' + item.id + '">' + tKey('async_jobs.result_loading') + '</pre></div>'
: '',
'</div>'
].join('');
}).join('');
list.querySelectorAll('[data-action="toggle"]').forEach(function(button) {
button.addEventListener('click', function () {
var jobId = this.getAttribute('data-job-id');
state.openId = state.openId === jobId ? null : jobId;
if (state.openId) {
void loadDetail(jobId);
} else {
render();
}
});
});
list.querySelectorAll('[data-action="cancel"]').forEach(function(button) {
button.addEventListener('click', async function () {
try {
var jobId = this.getAttribute('data-job-id');
await window.CrankApi.cancelAsyncJob(state.workspaceId, jobId);
if (window.CrankUi) {
window.CrankUi.success(tKey('async_jobs.toast.cancel.body'), tKey('async_jobs.toast.cancel.title'));
}
await load();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('async_jobs.toast.cancel_error.body'), tKey('async_jobs.toast.cancel_error.title'));
}
}
});
});
}
async function loadResult(jobId) {
try {
var result = await window.CrankApi.getAsyncJobResult(state.workspaceId, jobId);
var node = document.querySelector('[data-result-for="' + jobId + '"]');
if (node) node.textContent = formatJson(result);
} catch (error) {
var failedNode = document.querySelector('[data-result-for="' + jobId + '"]');
if (failedNode) failedNode.textContent = error.message || tKey('async_jobs.result_error');
}
}
async function loadDetail(jobId) {
var detail = await window.CrankApi.getAsyncJob(state.workspaceId, jobId);
state.items = state.items.map(function(item) {
return item.id === jobId ? detail : item;
});
render();
if (detail.status === 'completed' || detail.status === 'failed' || detail.status === 'cancelled' || detail.status === 'expired') {
await loadResult(jobId);
}
}
async function load() {
state.workspaceId = currentWorkspaceId();
if (!state.workspaceId) {
state.items = [];
state.error = tKey('async_jobs.error.workspace');
render();
return;
}
state.loading = true;
state.error = '';
render();
try {
var response = await window.CrankApi.listAsyncJobs(state.workspaceId, {
status: statusFilter.value || null,
page: 1,
page_size: 50,
});
state.items = response.items || [];
} catch (error) {
state.error = error.message || tKey('async_jobs.error.load');
} finally {
state.loading = false;
render();
}
}
refreshButton.addEventListener('click', load);
statusFilter.addEventListener('change', load);
document.addEventListener('workspace:changed', load);
void load();
});
+2
View File
@@ -8,6 +8,8 @@
apiKeys: '/api-keys',
logs: '/logs',
usage: '/usage',
streamSessions: '/stream-sessions',
asyncJobs: '/async-jobs',
settings: '/settings',
workspaceSetup: '/workspace-setup',
wizard: '/wizard/'
+234
View File
@@ -1569,6 +1569,240 @@ var TRANSLATIONS = {
}
};
Object.assign(TRANSLATIONS.en, {
'wizard.streaming.title': 'Streaming execution',
'wizard.streaming.subtitle': 'Bounded stream, session and async job settings for MCP tool families.',
'wizard.streaming.execution_mode': 'Execution mode',
'wizard.streaming.transport_behavior': 'Transport behavior',
'wizard.streaming.aggregation_mode': 'Aggregation mode',
'wizard.streaming.window_duration': 'Window duration (ms)',
'wizard.streaming.poll_interval': 'Poll interval (ms)',
'wizard.streaming.upstream_timeout': 'Upstream timeout (ms)',
'wizard.streaming.idle_timeout': 'Idle timeout (ms)',
'wizard.streaming.session_lifetime': 'Session lifetime (ms)',
'wizard.streaming.max_items': 'Max items',
'wizard.streaming.max_bytes': 'Max bytes',
'wizard.streaming.max_field_length': 'Max field length',
'wizard.streaming.sampling_rate': 'Sampling rate',
'wizard.streaming.items_path': 'Items path',
'wizard.streaming.summary_path': 'Summary path',
'wizard.streaming.done_path': 'Done path',
'wizard.streaming.cursor_path': 'Cursor path',
'wizard.streaming.status_path': 'Status path',
'wizard.streaming.redacted_paths': 'Redacted paths',
'wizard.streaming.truncate_item_fields': 'Truncate item fields',
'wizard.streaming.drop_duplicates': 'Drop duplicates',
'wizard.streaming.tool_family': 'Tool family',
'wizard.streaming.start_tool_name': 'Start tool name',
'wizard.streaming.poll_tool_name': 'Poll tool name',
'wizard.streaming.stop_tool_name': 'Stop tool name',
'wizard.streaming.status_tool_name': 'Status tool name',
'wizard.streaming.result_tool_name': 'Result tool name',
'wizard.streaming.cancel_tool_name': 'Cancel tool name',
'wizard.streaming.mode.unary': 'Unary',
'wizard.streaming.mode.window': 'Window',
'wizard.streaming.mode.session': 'Session',
'wizard.streaming.mode.async_job': 'Async job',
'wizard.streaming.transport.request_response': 'Request / response',
'wizard.streaming.transport.server_stream': 'Server stream',
'wizard.streaming.transport.stateful_session': 'Stateful session',
'wizard.streaming.transport.deferred_result': 'Deferred result',
'wizard.streaming.aggregation.raw_items': 'Raw items',
'wizard.streaming.aggregation.summary_only': 'Summary only',
'wizard.streaming.aggregation.summary_plus_samples': 'Summary + samples',
'wizard.streaming.aggregation.stats': 'Stats',
'wizard.streaming.aggregation.latest_state': 'Latest state',
'wizard.streaming.help.unary': 'Unary keeps the existing request-response model. Other modes create bounded stream-aware MCP tools.',
'wizard.streaming.help.window': 'Window mode collects a bounded slice of data and returns a compact result.',
'wizard.streaming.help.session': 'Session mode publishes start/poll/stop tool families backed by persisted stream sessions.',
'wizard.streaming.help.async_job': 'Async job mode publishes start/status/result/cancel tools and stores runtime job state.',
'stream_sessions.title': 'Stream sessions',
'stream_sessions.subtitle': 'Inspect stateful streaming sessions created by generated MCP tool families.',
'stream_sessions.refresh': 'Refresh',
'stream_sessions.summary': '{count} sessions',
'stream_sessions.filter.all_statuses': 'All statuses',
'stream_sessions.filter.all_modes': 'All modes',
'stream_sessions.loading.title': 'Loading stream sessions…',
'stream_sessions.loading.body': 'Fetching persisted session state for the current workspace.',
'stream_sessions.error.title': 'Unable to load stream sessions',
'stream_sessions.error.workspace': 'Workspace is not selected',
'stream_sessions.error.load': 'Failed to load stream sessions',
'stream_sessions.empty.title': 'No stream sessions found',
'stream_sessions.empty.body': 'Session-mode MCP tools will create entries here after start/poll/stop flows.',
'stream_sessions.operation': 'Operation: {operation}',
'stream_sessions.meta.created': 'Created',
'stream_sessions.meta.last_poll': 'Last poll',
'stream_sessions.meta.expires': 'Expires',
'stream_sessions.meta.agent': 'Agent',
'stream_sessions.cursor': 'Cursor',
'stream_sessions.stop': 'Stop session',
'stream_sessions.show_details': 'Show details',
'stream_sessions.hide_details': 'Hide details',
'stream_sessions.toast.stop.title': 'Session stopped',
'stream_sessions.toast.stop.body': 'The stream session was stopped.',
'stream_sessions.toast.stop_error.title': 'Stop failed',
'stream_sessions.toast.stop_error.body': 'Failed to stop stream session',
'stream_sessions.status.created': 'Created',
'stream_sessions.status.running': 'Running',
'stream_sessions.status.failed': 'Failed',
'stream_sessions.status.stopped': 'Stopped',
'stream_sessions.status.expired': 'Expired',
'async_jobs.title': 'Async jobs',
'async_jobs.subtitle': 'Inspect deferred jobs started by streaming MCP tool families.',
'async_jobs.refresh': 'Refresh',
'async_jobs.summary': '{count} jobs',
'async_jobs.filter.all_statuses': 'All statuses',
'async_jobs.loading.title': 'Loading async jobs…',
'async_jobs.loading.body': 'Fetching persisted job state for the current workspace.',
'async_jobs.error.title': 'Unable to load async jobs',
'async_jobs.error.workspace': 'Workspace is not selected',
'async_jobs.error.load': 'Failed to load async jobs',
'async_jobs.empty.title': 'No async jobs found',
'async_jobs.empty.body': 'Async job MCP tools will create entries here after start/status/result flows.',
'async_jobs.operation': 'Operation: {operation}',
'async_jobs.meta.created': 'Created',
'async_jobs.meta.updated': 'Updated',
'async_jobs.meta.finished': 'Finished',
'async_jobs.meta.expires': 'Expires',
'async_jobs.progress': 'Progress payload',
'async_jobs.error_preview': 'Error preview',
'async_jobs.result': 'Result payload',
'async_jobs.result_loading': 'Loading result…',
'async_jobs.result_error': 'Failed to load result',
'async_jobs.cancel': 'Cancel job',
'async_jobs.show_details': 'Show details',
'async_jobs.hide_details': 'Hide details',
'async_jobs.toast.cancel.title': 'Job cancelled',
'async_jobs.toast.cancel.body': 'The async job was cancelled.',
'async_jobs.toast.cancel_error.title': 'Cancel failed',
'async_jobs.toast.cancel_error.body': 'Failed to cancel async job',
'async_jobs.status.created': 'Created',
'async_jobs.status.running': 'Running',
'async_jobs.status.completed': 'Completed',
'async_jobs.status.failed': 'Failed',
'async_jobs.status.cancelled': 'Cancelled',
'async_jobs.status.expired': 'Expired',
});
Object.assign(TRANSLATIONS.ru, {
'wizard.streaming.title': 'Потоковое выполнение',
'wizard.streaming.subtitle': 'Ограниченные настройки стрима, сессий и async job для семейств MCP-инструментов.',
'wizard.streaming.execution_mode': 'Режим выполнения',
'wizard.streaming.transport_behavior': 'Поведение транспорта',
'wizard.streaming.aggregation_mode': 'Режим агрегации',
'wizard.streaming.window_duration': 'Длительность окна (мс)',
'wizard.streaming.poll_interval': 'Интервал poll (мс)',
'wizard.streaming.upstream_timeout': 'Таймаут upstream (мс)',
'wizard.streaming.idle_timeout': 'Таймаут простоя (мс)',
'wizard.streaming.session_lifetime': 'Время жизни сессии (мс)',
'wizard.streaming.max_items': 'Максимум элементов',
'wizard.streaming.max_bytes': 'Максимум байт',
'wizard.streaming.max_field_length': 'Максимальная длина поля',
'wizard.streaming.sampling_rate': 'Частота выборки',
'wizard.streaming.items_path': 'Путь к items',
'wizard.streaming.summary_path': 'Путь к summary',
'wizard.streaming.done_path': 'Путь к done',
'wizard.streaming.cursor_path': 'Путь к cursor',
'wizard.streaming.status_path': 'Путь к status',
'wizard.streaming.redacted_paths': 'Скрываемые пути',
'wizard.streaming.truncate_item_fields': 'Обрезать поля элементов',
'wizard.streaming.drop_duplicates': 'Удалять дубликаты',
'wizard.streaming.tool_family': 'Семейство инструментов',
'wizard.streaming.start_tool_name': 'Имя start-инструмента',
'wizard.streaming.poll_tool_name': 'Имя poll-инструмента',
'wizard.streaming.stop_tool_name': 'Имя stop-инструмента',
'wizard.streaming.status_tool_name': 'Имя status-инструмента',
'wizard.streaming.result_tool_name': 'Имя result-инструмента',
'wizard.streaming.cancel_tool_name': 'Имя cancel-инструмента',
'wizard.streaming.mode.unary': 'Unary',
'wizard.streaming.mode.window': 'Window',
'wizard.streaming.mode.session': 'Session',
'wizard.streaming.mode.async_job': 'Async job',
'wizard.streaming.transport.request_response': 'Запрос / ответ',
'wizard.streaming.transport.server_stream': 'Серверный стрим',
'wizard.streaming.transport.stateful_session': 'Состояние сессии',
'wizard.streaming.transport.deferred_result': 'Отложенный результат',
'wizard.streaming.aggregation.raw_items': 'Сырые элементы',
'wizard.streaming.aggregation.summary_only': 'Только summary',
'wizard.streaming.aggregation.summary_plus_samples': 'Summary + примеры',
'wizard.streaming.aggregation.stats': 'Статистика',
'wizard.streaming.aggregation.latest_state': 'Последнее состояние',
'wizard.streaming.help.unary': 'Unary сохраняет текущую модель запрос-ответ. Остальные режимы создают ограниченные потоковые MCP-инструменты.',
'wizard.streaming.help.window': 'Режим window собирает ограниченный срез данных и возвращает компактный результат.',
'wizard.streaming.help.session': 'Режим session публикует семейство start/poll/stop и опирается на сохраненные stream sessions.',
'wizard.streaming.help.async_job': 'Режим async job публикует start/status/result/cancel и хранит состояние фоновой задачи.',
'stream_sessions.title': 'Потоковые сессии',
'stream_sessions.subtitle': 'Просмотр stateful streaming-сессий, созданных сгенерированными семействами MCP-инструментов.',
'stream_sessions.refresh': 'Обновить',
'stream_sessions.summary': '{count} сессий',
'stream_sessions.filter.all_statuses': 'Все статусы',
'stream_sessions.filter.all_modes': 'Все режимы',
'stream_sessions.loading.title': 'Загрузка потоковых сессий…',
'stream_sessions.loading.body': 'Получаем сохраненное состояние сессий для текущего workspace.',
'stream_sessions.error.title': 'Не удалось загрузить потоковые сессии',
'stream_sessions.error.workspace': 'Workspace не выбран',
'stream_sessions.error.load': 'Не удалось загрузить потоковые сессии',
'stream_sessions.empty.title': 'Потоковых сессий пока нет',
'stream_sessions.empty.body': 'Сессии появятся здесь после вызовов start/poll/stop для session-mode инструментов.',
'stream_sessions.operation': 'Операция: {operation}',
'stream_sessions.meta.created': 'Создана',
'stream_sessions.meta.last_poll': 'Последний poll',
'stream_sessions.meta.expires': 'Истекает',
'stream_sessions.meta.agent': 'Агент',
'stream_sessions.cursor': 'Cursor',
'stream_sessions.stop': 'Остановить сессию',
'stream_sessions.show_details': 'Показать детали',
'stream_sessions.hide_details': 'Скрыть детали',
'stream_sessions.toast.stop.title': 'Сессия остановлена',
'stream_sessions.toast.stop.body': 'Потоковая сессия остановлена.',
'stream_sessions.toast.stop_error.title': 'Не удалось остановить',
'stream_sessions.toast.stop_error.body': 'Не удалось остановить потоковую сессию',
'stream_sessions.status.created': 'Создана',
'stream_sessions.status.running': 'Выполняется',
'stream_sessions.status.failed': 'Ошибка',
'stream_sessions.status.stopped': 'Остановлена',
'stream_sessions.status.expired': 'Истекла',
'async_jobs.title': 'Асинхронные задачи',
'async_jobs.subtitle': 'Просмотр отложенных задач, запущенных потоковыми семействами MCP-инструментов.',
'async_jobs.refresh': 'Обновить',
'async_jobs.summary': '{count} задач',
'async_jobs.filter.all_statuses': 'Все статусы',
'async_jobs.loading.title': 'Загрузка async job…',
'async_jobs.loading.body': 'Получаем сохраненное состояние задач для текущего workspace.',
'async_jobs.error.title': 'Не удалось загрузить async job',
'async_jobs.error.workspace': 'Workspace не выбран',
'async_jobs.error.load': 'Не удалось загрузить async job',
'async_jobs.empty.title': 'Асинхронных задач пока нет',
'async_jobs.empty.body': 'Задачи появятся здесь после вызовов start/status/result для async job инструментов.',
'async_jobs.operation': 'Операция: {operation}',
'async_jobs.meta.created': 'Создана',
'async_jobs.meta.updated': 'Обновлена',
'async_jobs.meta.finished': 'Завершена',
'async_jobs.meta.expires': 'Истекает',
'async_jobs.progress': 'Payload прогресса',
'async_jobs.error_preview': 'Предпросмотр ошибки',
'async_jobs.result': 'Payload результата',
'async_jobs.result_loading': 'Загрузка результата…',
'async_jobs.result_error': 'Не удалось загрузить результат',
'async_jobs.cancel': 'Отменить задачу',
'async_jobs.show_details': 'Показать детали',
'async_jobs.hide_details': 'Скрыть детали',
'async_jobs.toast.cancel.title': 'Задача отменена',
'async_jobs.toast.cancel.body': 'Асинхронная задача отменена.',
'async_jobs.toast.cancel_error.title': 'Не удалось отменить',
'async_jobs.toast.cancel_error.body': 'Не удалось отменить async job',
'async_jobs.status.created': 'Создана',
'async_jobs.status.running': 'Выполняется',
'async_jobs.status.completed': 'Завершена',
'async_jobs.status.failed': 'Ошибка',
'async_jobs.status.cancelled': 'Отменена',
'async_jobs.status.expired': 'Истекла',
});
function t(key) {
var lang = localStorage.getItem('crank_lang') || 'en';
var tr = TRANSLATIONS[lang] || TRANSLATIONS.en;
+166
View File
@@ -0,0 +1,166 @@
document.addEventListener('DOMContentLoaded', function () {
var state = {
items: [],
workspaceId: null,
loading: false,
error: '',
openId: null,
};
var list = document.getElementById('stream-sessions-list');
var summary = document.getElementById('stream-sessions-summary');
var refreshButton = document.getElementById('stream-sessions-refresh');
var statusFilter = document.getElementById('stream-sessions-status');
var modeFilter = document.getElementById('stream-sessions-mode');
function tKey(key, vars) {
if (!window.t) return key;
return t(key, vars);
}
function currentWorkspaceId() {
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
return workspace ? workspace.id : null;
}
function formatJson(value) {
if (value === null || value === undefined) return '';
if (typeof value === 'string') return value;
return JSON.stringify(value, null, 2);
}
function formatDateTime(value) {
if (!value) return '—';
return new Date(value).toLocaleString();
}
function renderEmpty(title, body) {
list.innerHTML = '<div class="empty-state"><div class="empty-state-title">' + title + '</div><div class="empty-state-text">' + body + '</div></div>';
}
function render() {
summary.textContent = tKey('stream_sessions.summary', { count: state.items.length });
if (state.loading && state.items.length === 0) {
renderEmpty(tKey('stream_sessions.loading.title'), tKey('stream_sessions.loading.body'));
return;
}
if (state.error) {
renderEmpty(tKey('stream_sessions.error.title'), state.error);
return;
}
if (!state.items.length) {
renderEmpty(tKey('stream_sessions.empty.title'), tKey('stream_sessions.empty.body'));
return;
}
list.innerHTML = state.items.map(function(item) {
var open = state.openId === item.id;
var statusLabel = tKey('stream_sessions.status.' + item.status);
return [
'<div class="resource-card" data-session-id="' + item.id + '">',
' <div class="resource-card-header">',
' <div>',
' <div class="resource-card-title">' + item.id + '</div>',
' <div class="resource-card-subtitle">' + tKey('stream_sessions.operation', { operation: item.operation_id }) + '</div>',
' <div class="resource-pill-row">',
' <span class="resource-status-pill ' + String(item.status || '').toLowerCase() + '">' + statusLabel + '</span>',
' <span class="resource-status-pill">' + item.mode + '</span>',
' <span class="resource-status-pill">' + item.protocol + '</span>',
' </div>',
' </div>',
' <div class="resource-card-actions">',
item.status === 'created' || item.status === 'running'
? ' <button class="btn-secondary" data-action="stop" data-session-id="' + item.id + '">' + tKey('stream_sessions.stop') + '</button>'
: '',
' <button class="btn-secondary" data-action="toggle" data-session-id="' + item.id + '">' + (open ? tKey('stream_sessions.hide_details') : tKey('stream_sessions.show_details')) + '</button>',
' </div>',
' </div>',
' <div class="resource-meta-grid">',
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('stream_sessions.meta.created') + '</div><div class="resource-meta-value">' + formatDateTime(item.created_at) + '</div></div>',
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('stream_sessions.meta.last_poll') + '</div><div class="resource-meta-value">' + formatDateTime(item.last_poll_at) + '</div></div>',
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('stream_sessions.meta.expires') + '</div><div class="resource-meta-value">' + formatDateTime(item.expires_at) + '</div></div>',
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('stream_sessions.meta.agent') + '</div><div class="resource-meta-value">' + (item.agent_id || '—') + '</div></div>',
' </div>',
open
? ' <div class="resource-detail-block"><div class="resource-detail-title">' + tKey('stream_sessions.cursor') + '</div><pre class="resource-detail-pre">' + formatJson(item.cursor) + '</pre></div>'
: '',
'</div>'
].join('');
}).join('');
list.querySelectorAll('[data-action="toggle"]').forEach(function(button) {
button.addEventListener('click', function () {
var sessionId = this.getAttribute('data-session-id');
state.openId = state.openId === sessionId ? null : sessionId;
if (state.openId) {
void loadDetail(sessionId);
} else {
render();
}
});
});
list.querySelectorAll('[data-action="stop"]').forEach(function(button) {
button.addEventListener('click', async function () {
try {
var sessionId = this.getAttribute('data-session-id');
await window.CrankApi.stopStreamSession(state.workspaceId, sessionId);
if (window.CrankUi) {
window.CrankUi.success(tKey('stream_sessions.toast.stop.body'), tKey('stream_sessions.toast.stop.title'));
}
await load();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('stream_sessions.toast.stop_error.body'), tKey('stream_sessions.toast.stop_error.title'));
}
}
});
});
}
async function loadDetail(sessionId) {
var detail = await window.CrankApi.getStreamSession(state.workspaceId, sessionId);
state.items = state.items.map(function(item) {
return item.id === sessionId ? detail : item;
});
render();
}
async function load() {
state.workspaceId = currentWorkspaceId();
if (!state.workspaceId) {
state.items = [];
state.error = tKey('stream_sessions.error.workspace');
render();
return;
}
state.loading = true;
state.error = '';
render();
try {
var response = await window.CrankApi.listStreamSessions(state.workspaceId, {
status: statusFilter.value || null,
mode: modeFilter.value || null,
page: 1,
page_size: 50,
});
state.items = response.items || [];
} catch (error) {
state.error = error.message || tKey('stream_sessions.error.load');
} finally {
state.loading = false;
render();
}
}
refreshButton.addEventListener('click', load);
statusFilter.addEventListener('change', load);
modeFilter.addEventListener('change', load);
document.addEventListener('workspace:changed', load);
void load();
});
+47
View File
@@ -0,0 +1,47 @@
(function() {
function currentWorkspaceId() {
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
return workspace ? workspace.id : null;
}
async function startWindowTest(operationId, payload) {
if (!window.CrankApi || !operationId) {
throw new Error('Window test requires an operation id.');
}
return window.CrankApi.runOperationTest(currentWorkspaceId(), operationId, payload);
}
async function startSessionTest(workspaceId, operationId, payload) {
return window.CrankApi.runOperationTest(workspaceId || currentWorkspaceId(), operationId, payload);
}
async function pollSessionTest(workspaceId, sessionId) {
return window.CrankApi.getStreamSession(workspaceId || currentWorkspaceId(), sessionId);
}
async function stopSessionTest(workspaceId, sessionId) {
return window.CrankApi.stopStreamSession(workspaceId || currentWorkspaceId(), sessionId);
}
async function startAsyncJobTest(workspaceId, operationId, payload) {
return window.CrankApi.runOperationTest(workspaceId || currentWorkspaceId(), operationId, payload);
}
async function refreshAsyncJobStatus(workspaceId, jobId) {
return window.CrankApi.getAsyncJob(workspaceId || currentWorkspaceId(), jobId);
}
async function loadAsyncJobResult(workspaceId, jobId) {
return window.CrankApi.getAsyncJobResult(workspaceId || currentWorkspaceId(), jobId);
}
window.CrankStreamTestRun = {
startWindowTest: startWindowTest,
startSessionTest: startSessionTest,
pollSessionTest: pollSessionTest,
stopSessionTest: stopSessionTest,
startAsyncJobTest: startAsyncJobTest,
refreshAsyncJobStatus: refreshAsyncJobStatus,
loadAsyncJobResult: loadAsyncJobResult,
};
}());
+127
View File
@@ -0,0 +1,127 @@
(function() {
function text(id) {
var element = document.getElementById(id);
return element ? String(element.value || '').trim() : '';
}
function number(id) {
var value = text(id);
if (!value) return null;
var parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function lines(id) {
var value = text(id);
return value
? value.split('\n').map(function(item) { return item.trim(); }).filter(Boolean)
: [];
}
function checked(id) {
var element = document.getElementById(id);
return !!(element && element.checked);
}
function selectedMode() {
return text('streaming-mode') || 'unary';
}
function serializeStreamingConfig() {
if (selectedMode() === 'unary') {
return null;
}
var config = {
mode: selectedMode(),
transport_behavior: text('streaming-transport-behavior') || 'request_response',
aggregation_mode: text('streaming-aggregation-mode') || 'summary_plus_samples',
window_duration_ms: number('streaming-window-duration-ms'),
poll_interval_ms: number('streaming-poll-interval-ms'),
upstream_timeout_ms: number('streaming-upstream-timeout-ms'),
idle_timeout_ms: number('streaming-idle-timeout-ms'),
max_session_lifetime_ms: number('streaming-session-lifetime-ms'),
max_items: number('streaming-max-items'),
max_bytes: number('streaming-max-bytes'),
max_field_length: number('streaming-max-field-length'),
sampling_rate: number('streaming-sampling-rate'),
items_path: text('streaming-items-path') || null,
summary_path: text('streaming-summary-path') || null,
done_path: text('streaming-done-path') || null,
cursor_path: text('streaming-cursor-path') || null,
status_path: text('streaming-status-path') || null,
redacted_paths: lines('streaming-redacted-paths'),
truncate_item_fields: checked('streaming-truncate-item-fields'),
drop_duplicates: checked('streaming-drop-duplicates'),
tool_family: {},
};
if (config.mode === 'session') {
config.tool_family.start_tool_name = text('streaming-start-tool-name') || null;
config.tool_family.poll_tool_name = text('streaming-poll-tool-name') || null;
config.tool_family.stop_tool_name = text('streaming-stop-tool-name') || null;
} else if (config.mode === 'async_job') {
config.tool_family.start_tool_name = text('streaming-start-tool-name') || null;
config.tool_family.status_tool_name = text('streaming-status-tool-name') || null;
config.tool_family.result_tool_name = text('streaming-result-tool-name') || null;
config.tool_family.cancel_tool_name = text('streaming-cancel-tool-name') || null;
}
return config;
}
function validateStreamingConfig(capabilities) {
var config = serializeStreamingConfig();
if (!config) {
return { valid: true, errors: [] };
}
var errors = [];
var capability = (capabilities || []).find(function(item) {
return item.protocol === window.wizardProtocol;
});
if (capability && Array.isArray(capability.supports_execution_modes)) {
var supported = capability.supports_execution_modes.map(String);
if (supported.indexOf(config.mode) === -1) {
errors.push('Selected execution mode is not supported by the current protocol.');
}
}
if (!config.max_items || config.max_items <= 0) {
errors.push('Max items must be a positive integer.');
}
if (!config.max_bytes || config.max_bytes <= 0) {
errors.push('Max bytes must be a positive integer.');
}
if (config.mode === 'window' && (!config.window_duration_ms || config.window_duration_ms <= 0)) {
errors.push('Window duration must be set for window mode.');
}
if (config.mode === 'session' && (!config.poll_interval_ms || config.poll_interval_ms <= 0)) {
errors.push('Poll interval must be set for session mode.');
}
if (config.mode === 'async_job' && (!config.max_session_lifetime_ms || config.max_session_lifetime_ms <= 0)) {
errors.push('Session lifetime must be set for async job mode.');
}
return {
valid: errors.length === 0,
errors: errors,
config: config,
};
}
window.CrankStreamingForm = {
serializeStreamingConfig: serializeStreamingConfig,
deserializeStreamingConfig: function(streaming) {
if (typeof window.applyStreamingConfig === 'function') {
window.applyStreamingConfig(streaming);
}
},
validateStreamingConfig: validateStreamingConfig,
};
}());
+314
View File
@@ -10,6 +10,7 @@ var wizardProtoUpload = null;
var wizardDescriptorSetUpload = null;
var wizardTestResponsePreview = null;
var grpcDescriptorServices = [];
var wizardProtocolCapabilities = null;
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
@@ -17,6 +18,48 @@ function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : key;
}
function defaultProtocolCapabilities() {
return {
rest: {
supports_execution_modes: ['unary', 'window', 'session', 'async_job'],
supports_transport_behaviors: ['request_response', 'server_stream', 'deferred_result'],
},
graphql: {
supports_execution_modes: ['unary'],
supports_transport_behaviors: ['request_response'],
},
grpc: {
supports_execution_modes: ['unary', 'window', 'session', 'async_job'],
supports_transport_behaviors: ['request_response', 'server_stream'],
},
};
}
function currentProtocolCapabilities() {
var capabilities = wizardProtocolCapabilities || defaultProtocolCapabilities();
return capabilities[wizardProtocol] || capabilities.rest;
}
async function loadProtocolCapabilities() {
if (!wizardWorkspaceId || !window.CrankApi || typeof window.CrankApi.getProtocolCapabilities !== 'function') {
wizardProtocolCapabilities = defaultProtocolCapabilities();
return wizardProtocolCapabilities;
}
try {
var response = await window.CrankApi.getProtocolCapabilities(wizardWorkspaceId);
var mapped = {};
(response.items || []).forEach(function(item) {
mapped[item.protocol] = item;
});
wizardProtocolCapabilities = mapped;
} catch (_error) {
wizardProtocolCapabilities = defaultProtocolCapabilities();
}
return wizardProtocolCapabilities;
}
/* ── Dynamic step loading ── */
function _stepFile(n) {
return (n === 3) ? 'step3-' + wizardProtocol + '.html' : 'step' + n + '.html';
@@ -222,10 +265,12 @@ document.addEventListener('DOMContentLoaded', async function() {
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
wizardWorkspaceId = workspace ? workspace.id : null;
await loadProtocolCapabilities();
await loadWizardPanels([1, 2, 3, 4, 5]);
bindProtocolCards();
bindWizardLiveActions();
bindStreamingConfigControls();
var params = new URLSearchParams(window.location.search);
if (params.get('mode') === 'edit' && params.get('operationId')) {
@@ -1092,6 +1137,12 @@ function buildMappingSet(rawValue, mode) {
}
function parseExecutionConfig(text) {
if (window.CrankStreamingForm && typeof window.CrankStreamingForm.validateStreamingConfig === 'function') {
var validation = window.CrankStreamingForm.validateStreamingConfig([currentProtocolCapabilities()]);
if (!validation.valid) {
throw new Error(validation.errors[0]);
}
}
var value = parseStructuredText(text);
var retry = value.retry || value.retry_policy || null;
var headers = value.headers && typeof value.headers === 'object' ? value.headers : {};
@@ -1113,6 +1164,8 @@ function parseExecutionConfig(text) {
config.protocol_options = { grpc: { use_tls: useTls } };
}
config.streaming = collectStreamingConfig();
return config;
}
@@ -1236,6 +1289,264 @@ function parseHeaderMap(text) {
return value && typeof value === 'object' ? value : {};
}
function numericValueOrNull(id) {
var value = textValue(id);
if (!value) return null;
var parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
function stringValueOrNull(id) {
var value = textValue(id);
return value ? value : null;
}
function textareaLines(id) {
var value = textValue(id);
if (!value) return [];
return value
.split(/\r?\n/)
.map(function(line) { return line.trim(); })
.filter(Boolean);
}
function selectedStreamingMode() {
var element = document.getElementById('streaming-mode');
return element ? element.value : 'unary';
}
function transportOptionsForMode(mode) {
var capabilities = currentProtocolCapabilities();
var behaviors = (capabilities.supports_transport_behaviors || []).slice();
if (mode === 'unary') return behaviors.filter(function(value) { return value === 'request_response'; });
if (mode === 'window') return behaviors.filter(function(value) {
return value === 'request_response' || value === 'server_stream';
});
if (mode === 'session') return behaviors.filter(function(value) {
return value === 'server_stream' || value === 'stateful_session';
});
if (mode === 'async_job') return behaviors.filter(function(value) {
return value === 'deferred_result';
});
return behaviors;
}
function modeOptionsForProtocol() {
var capabilities = currentProtocolCapabilities();
return (capabilities.supports_execution_modes || ['unary']).filter(function(mode) {
return transportOptionsForMode(mode).length > 0 || mode === 'unary';
});
}
function updateStreamingTransportOptions(preferredValue) {
var element = document.getElementById('streaming-transport-behavior');
if (!element) return;
var options = transportOptionsForMode(selectedStreamingMode());
element.innerHTML = '';
options.forEach(function(value) {
var option = document.createElement('option');
option.value = value;
option.textContent = tKey('wizard.streaming.transport.' + value);
element.appendChild(option);
});
if (preferredValue && options.indexOf(preferredValue) >= 0) {
element.value = preferredValue;
} else if (options.length) {
element.value = options[0];
}
}
function updateStreamingModeOptions(preferredValue) {
var element = document.getElementById('streaming-mode');
if (!element) return;
var options = modeOptionsForProtocol();
element.innerHTML = '';
options.forEach(function(value) {
var option = document.createElement('option');
option.value = value;
option.textContent = tKey('wizard.streaming.mode.' + value);
element.appendChild(option);
});
if (preferredValue && options.indexOf(preferredValue) >= 0) {
element.value = preferredValue;
} else {
element.value = options.indexOf('unary') >= 0 ? 'unary' : (options[0] || 'unary');
}
updateStreamingTransportOptions();
}
function ensureStreamingToolFamilyDefaults(mode) {
var base = textValue('tool-name') || 'stream_tool';
if (mode === 'session') {
if (!textValue('streaming-start-tool-name')) setValue('streaming-start-tool-name', base + '_start');
if (!textValue('streaming-poll-tool-name')) setValue('streaming-poll-tool-name', base + '_poll');
if (!textValue('streaming-stop-tool-name')) setValue('streaming-stop-tool-name', base + '_stop');
}
if (mode === 'async_job') {
if (!textValue('streaming-start-tool-name')) setValue('streaming-start-tool-name', base + '_start');
if (!textValue('streaming-status-tool-name')) setValue('streaming-status-tool-name', base + '_status');
if (!textValue('streaming-result-tool-name')) setValue('streaming-result-tool-name', base + '_result');
if (!textValue('streaming-cancel-tool-name')) setValue('streaming-cancel-tool-name', base + '_cancel');
}
}
function updateStreamingConfigVisibility() {
var mode = selectedStreamingMode();
var card = document.getElementById('wizard-streaming-config-card');
var help = document.getElementById('streaming-mode-help');
var toolFamily = document.getElementById('streaming-tool-family-block');
var asyncRow = document.getElementById('streaming-async-tool-family-row');
if (!card) return;
var isUnary = mode === 'unary';
var isWindow = mode === 'window';
var isSession = mode === 'session';
var isAsyncJob = mode === 'async_job';
[
'streaming-window-duration-ms',
'streaming-poll-interval-ms',
'streaming-upstream-timeout-ms',
'streaming-idle-timeout-ms',
'streaming-session-lifetime-ms',
'streaming-max-items',
'streaming-max-bytes',
'streaming-max-field-length',
'streaming-sampling-rate',
'streaming-items-path',
'streaming-summary-path',
'streaming-done-path',
'streaming-cursor-path',
'streaming-status-path',
'streaming-redacted-paths',
'streaming-truncate-item-fields',
'streaming-drop-duplicates'
].forEach(function(id) {
var node = document.getElementById(id);
if (!node) return;
var group = node.closest('.form-group') || node.closest('.checkbox-pill');
if (group) {
group.style.display = isUnary ? 'none' : '';
}
});
if (help) {
help.textContent = isUnary
? tKey('wizard.streaming.help.unary')
: (isSession
? tKey('wizard.streaming.help.session')
: (isAsyncJob
? tKey('wizard.streaming.help.async_job')
: tKey('wizard.streaming.help.window')));
}
if (toolFamily) toolFamily.style.display = (isSession || isAsyncJob) ? '' : 'none';
if (asyncRow) asyncRow.style.display = isAsyncJob ? '' : 'none';
ensureStreamingToolFamilyDefaults(mode);
}
function bindStreamingConfigControls() {
updateStreamingModeOptions();
updateStreamingConfigVisibility();
var mode = document.getElementById('streaming-mode');
if (mode) {
mode.addEventListener('change', function() {
updateStreamingTransportOptions();
updateStreamingConfigVisibility();
});
}
}
function collectStreamingConfig() {
var mode = selectedStreamingMode();
if (mode === 'unary') return null;
var config = {
mode: mode,
transport_behavior: stringValueOrNull('streaming-transport-behavior') || 'request_response',
window_duration_ms: numericValueOrNull('streaming-window-duration-ms'),
poll_interval_ms: numericValueOrNull('streaming-poll-interval-ms'),
upstream_timeout_ms: numericValueOrNull('streaming-upstream-timeout-ms'),
idle_timeout_ms: numericValueOrNull('streaming-idle-timeout-ms'),
max_session_lifetime_ms: numericValueOrNull('streaming-session-lifetime-ms'),
max_items: numericValueOrNull('streaming-max-items'),
max_bytes: numericValueOrNull('streaming-max-bytes'),
aggregation_mode: stringValueOrNull('streaming-aggregation-mode') || 'summary_plus_samples',
summary_path: stringValueOrNull('streaming-summary-path'),
items_path: stringValueOrNull('streaming-items-path'),
cursor_path: stringValueOrNull('streaming-cursor-path'),
status_path: stringValueOrNull('streaming-status-path'),
done_path: stringValueOrNull('streaming-done-path'),
redacted_paths: textareaLines('streaming-redacted-paths'),
truncate_item_fields: !!document.getElementById('streaming-truncate-item-fields').checked,
max_field_length: numericValueOrNull('streaming-max-field-length'),
drop_duplicates: !!document.getElementById('streaming-drop-duplicates').checked,
sampling_rate: (function() {
var value = textValue('streaming-sampling-rate');
if (!value) return null;
var parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}()),
tool_family: {},
};
if (mode === 'session') {
config.tool_family.start_tool_name = stringValueOrNull('streaming-start-tool-name');
config.tool_family.poll_tool_name = stringValueOrNull('streaming-poll-tool-name');
config.tool_family.stop_tool_name = stringValueOrNull('streaming-stop-tool-name');
} else if (mode === 'async_job') {
config.tool_family.start_tool_name = stringValueOrNull('streaming-start-tool-name');
config.tool_family.status_tool_name = stringValueOrNull('streaming-status-tool-name');
config.tool_family.result_tool_name = stringValueOrNull('streaming-result-tool-name');
config.tool_family.cancel_tool_name = stringValueOrNull('streaming-cancel-tool-name');
}
Object.keys(config).forEach(function(key) {
if (config[key] === null) delete config[key];
});
if (!config.redacted_paths.length) delete config.redacted_paths;
return config;
}
function applyStreamingConfig(streaming) {
updateStreamingModeOptions(streaming && streaming.mode ? streaming.mode : 'unary');
if (!streaming) {
updateStreamingConfigVisibility();
return;
}
setValue('streaming-window-duration-ms', streaming.window_duration_ms || '');
setValue('streaming-poll-interval-ms', streaming.poll_interval_ms || '');
setValue('streaming-upstream-timeout-ms', streaming.upstream_timeout_ms || '');
setValue('streaming-idle-timeout-ms', streaming.idle_timeout_ms || '');
setValue('streaming-session-lifetime-ms', streaming.max_session_lifetime_ms || '');
setValue('streaming-max-items', streaming.max_items || '');
setValue('streaming-max-bytes', streaming.max_bytes || '');
setValue('streaming-max-field-length', streaming.max_field_length || '');
setValue('streaming-sampling-rate', streaming.sampling_rate || '');
setValue('streaming-items-path', streaming.items_path || '');
setValue('streaming-summary-path', streaming.summary_path || '');
setValue('streaming-done-path', streaming.done_path || '');
setValue('streaming-cursor-path', streaming.cursor_path || '');
setValue('streaming-status-path', streaming.status_path || '');
setValue('streaming-redacted-paths', (streaming.redacted_paths || []).join('\n'));
document.getElementById('streaming-truncate-item-fields').checked = !!streaming.truncate_item_fields;
document.getElementById('streaming-drop-duplicates').checked = !!streaming.drop_duplicates;
updateStreamingTransportOptions(streaming.transport_behavior);
setValue('streaming-start-tool-name', streaming.tool_family && streaming.tool_family.start_tool_name ? streaming.tool_family.start_tool_name : '');
setValue('streaming-poll-tool-name', streaming.tool_family && streaming.tool_family.poll_tool_name ? streaming.tool_family.poll_tool_name : '');
setValue('streaming-stop-tool-name', streaming.tool_family && streaming.tool_family.stop_tool_name ? streaming.tool_family.stop_tool_name : '');
setValue('streaming-status-tool-name', streaming.tool_family && streaming.tool_family.status_tool_name ? streaming.tool_family.status_tool_name : '');
setValue('streaming-result-tool-name', streaming.tool_family && streaming.tool_family.result_tool_name ? streaming.tool_family.result_tool_name : '');
setValue('streaming-cancel-tool-name', streaming.tool_family && streaming.tool_family.cancel_tool_name ? streaming.tool_family.cancel_tool_name : '');
var aggregation = document.getElementById('streaming-aggregation-mode');
if (aggregation && streaming.aggregation_mode) aggregation.value = streaming.aggregation_mode;
updateStreamingConfigVisibility();
}
function buildToolDescription() {
return {
title: textValue('tool-title') || textValue('tool-display-name') || textValue('tool-name'),
@@ -1439,6 +1750,7 @@ function prefillWizardFromEdit(detail, versionDocument) {
setValue('tool-input-mapping', mappingSetToEditorValue(versionDocument.input_mapping, 'input', detail.protocol));
setValue('tool-output-mapping', mappingSetToEditorValue(versionDocument.output_mapping, 'output', detail.protocol));
setValue('tool-exec-config', executionConfigToEditorValue(versionDocument.execution_config));
applyStreamingConfig(versionDocument.execution_config ? versionDocument.execution_config.streaming : null);
var toolNameInput = document.getElementById('tool-name');
if (toolNameInput) toolNameInput.disabled = true;
@@ -1525,6 +1837,8 @@ async function runWizardLiveAction(button, busyLabel, handler) {
function updateWizardProtocolVisibility() {
var grpcTools = document.getElementById('wizard-grpc-live-tools');
if (grpcTools) grpcTools.style.display = wizardProtocol === 'grpc' ? '' : 'none';
updateStreamingModeOptions();
updateStreamingConfigVisibility();
}
function showWizardLiveStatus(title, text, isError) {
+16
View File
@@ -34,6 +34,14 @@ server {
try_files /html/usage.html =404;
}
location = /stream-sessions {
try_files /html/stream-sessions.html =404;
}
location = /async-jobs {
try_files /html/async-jobs.html =404;
}
location = /settings {
try_files /html/settings.html =404;
}
@@ -75,6 +83,14 @@ server {
return 302 /usage;
}
location = /html/stream-sessions.html {
return 302 /stream-sessions;
}
location = /html/async-jobs.html {
return 302 /async-jobs;
}
location = /html/settings.html {
return 302 /settings;
}