community: remove premium protocols and streaming surface
This commit is contained in:
@@ -1,106 +0,0 @@
|
||||
<!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="%CRANK_BUNDLE_PROTECTED_CORE%"></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" hidden>
|
||||
<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="/secrets" class="nav-link" data-i18n="nav.secrets">Secrets</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>
|
||||
<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" hidden>
|
||||
<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="/secrets" class="mobile-nav-link" data-i18n="nav.secrets">Secrets</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="%CRANK_BUNDLE_ASYNC_JOBS%"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,111 +0,0 @@
|
||||
<!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="%CRANK_BUNDLE_PROTECTED_CORE%"></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" hidden>
|
||||
<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="/secrets" class="nav-link" data-i18n="nav.secrets">Secrets</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>
|
||||
<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" hidden>
|
||||
<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="/secrets" class="mobile-nav-link" data-i18n="nav.secrets">Secrets</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="%CRANK_BUNDLE_STREAM_SESSIONS%"></script>
|
||||
</body>
|
||||
</html>
|
||||
+2
-2
@@ -173,13 +173,13 @@
|
||||
<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','graphql','grpc','websocket','soap']" :key="p">
|
||||
<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 === 'graphql' ? 'GraphQL' : p === 'grpc' ? 'gRPC' : p === 'websocket' ? 'WebSocket' : 'SOAP'"></span>
|
||||
<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>
|
||||
|
||||
@@ -376,27 +376,6 @@
|
||||
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)
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
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 element(tag, className, text) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text !== undefined && text !== null) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function renderEmpty(title, body) {
|
||||
list.innerHTML = '';
|
||||
var empty = element('div', 'empty-state');
|
||||
empty.appendChild(element('div', 'empty-state-title', title));
|
||||
empty.appendChild(element('div', 'empty-state-text', body));
|
||||
list.appendChild(empty);
|
||||
}
|
||||
|
||||
function appendMetaItem(parent, label, value) {
|
||||
var item = element('div', 'resource-meta-item');
|
||||
item.appendChild(element('div', 'resource-meta-label', label));
|
||||
item.appendChild(element('div', 'resource-meta-value', value));
|
||||
parent.appendChild(item);
|
||||
}
|
||||
|
||||
function appendDetailBlock(parent, title, value, dataResultFor) {
|
||||
var block = element('div', 'resource-detail-block');
|
||||
block.appendChild(element('div', 'resource-detail-title', title));
|
||||
var pre = element('pre', 'resource-detail-pre', value);
|
||||
if (dataResultFor) pre.setAttribute('data-result-for', dataResultFor);
|
||||
block.appendChild(pre);
|
||||
parent.appendChild(block);
|
||||
}
|
||||
|
||||
function renderJobCard(item) {
|
||||
var open = state.openId === item.id;
|
||||
var card = element('div', 'resource-card');
|
||||
card.setAttribute('data-job-id', item.id);
|
||||
|
||||
var header = element('div', 'resource-card-header');
|
||||
var headerMain = element('div');
|
||||
headerMain.appendChild(element('div', 'resource-card-title', item.id));
|
||||
headerMain.appendChild(element('div', 'resource-card-subtitle', tKey('async_jobs.operation', { operation: item.operation_id })));
|
||||
|
||||
var pillRow = element('div', 'resource-pill-row');
|
||||
pillRow.appendChild(
|
||||
element('span', 'resource-status-pill ' + String(item.status || '').toLowerCase(), tKey('async_jobs.status.' + item.status))
|
||||
);
|
||||
headerMain.appendChild(pillRow);
|
||||
header.appendChild(headerMain);
|
||||
|
||||
var actions = element('div', 'resource-card-actions');
|
||||
if (item.status === 'created' || item.status === 'running') {
|
||||
var cancelButton = element('button', 'btn-secondary', tKey('async_jobs.cancel'));
|
||||
cancelButton.setAttribute('data-action', 'cancel');
|
||||
cancelButton.setAttribute('data-job-id', item.id);
|
||||
actions.appendChild(cancelButton);
|
||||
}
|
||||
var toggleButton = element(
|
||||
'button',
|
||||
'btn-secondary',
|
||||
open ? tKey('async_jobs.hide_details') : tKey('async_jobs.show_details')
|
||||
);
|
||||
toggleButton.setAttribute('data-action', 'toggle');
|
||||
toggleButton.setAttribute('data-job-id', item.id);
|
||||
actions.appendChild(toggleButton);
|
||||
header.appendChild(actions);
|
||||
card.appendChild(header);
|
||||
|
||||
var metaGrid = element('div', 'resource-meta-grid');
|
||||
appendMetaItem(metaGrid, tKey('async_jobs.meta.created'), formatDateTime(item.created_at));
|
||||
appendMetaItem(metaGrid, tKey('async_jobs.meta.updated'), formatDateTime(item.updated_at));
|
||||
appendMetaItem(metaGrid, tKey('async_jobs.meta.finished'), formatDateTime(item.finished_at));
|
||||
appendMetaItem(metaGrid, tKey('async_jobs.meta.expires'), formatDateTime(item.expires_at));
|
||||
card.appendChild(metaGrid);
|
||||
|
||||
if (open) {
|
||||
appendDetailBlock(card, tKey('async_jobs.progress'), formatJson(item.progress));
|
||||
appendDetailBlock(card, tKey('async_jobs.error_preview'), formatJson(item.error));
|
||||
if (
|
||||
item.status === 'completed' ||
|
||||
item.status === 'failed' ||
|
||||
item.status === 'cancelled' ||
|
||||
item.status === 'expired'
|
||||
) {
|
||||
appendDetailBlock(card, tKey('async_jobs.result'), tKey('async_jobs.result_loading'), item.id);
|
||||
}
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
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.forEach(function(item) {
|
||||
list.appendChild(renderJobCard(item));
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
@@ -9,8 +9,6 @@
|
||||
secrets: '/secrets',
|
||||
logs: '/logs',
|
||||
usage: '/usage',
|
||||
streamSessions: '/stream-sessions',
|
||||
asyncJobs: '/async-jobs',
|
||||
settings: '/settings',
|
||||
workspaceSetup: '/workspace-setup',
|
||||
wizard: '/wizard/'
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
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 element(tag, className, text) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text !== undefined && text !== null) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function renderEmpty(title, body) {
|
||||
list.innerHTML = '';
|
||||
var empty = element('div', 'empty-state');
|
||||
empty.appendChild(element('div', 'empty-state-title', title));
|
||||
empty.appendChild(element('div', 'empty-state-text', body));
|
||||
list.appendChild(empty);
|
||||
}
|
||||
|
||||
function appendMetaItem(parent, label, value) {
|
||||
var item = element('div', 'resource-meta-item');
|
||||
item.appendChild(element('div', 'resource-meta-label', label));
|
||||
item.appendChild(element('div', 'resource-meta-value', value));
|
||||
parent.appendChild(item);
|
||||
}
|
||||
|
||||
function appendDetailBlock(parent, title, value) {
|
||||
var block = element('div', 'resource-detail-block');
|
||||
block.appendChild(element('div', 'resource-detail-title', title));
|
||||
block.appendChild(element('pre', 'resource-detail-pre', formatJson(value)));
|
||||
parent.appendChild(block);
|
||||
}
|
||||
|
||||
function renderSessionCard(item) {
|
||||
var open = state.openId === item.id;
|
||||
var card = element('div', 'resource-card');
|
||||
card.setAttribute('data-session-id', item.id);
|
||||
|
||||
var header = element('div', 'resource-card-header');
|
||||
var headerMain = element('div');
|
||||
headerMain.appendChild(element('div', 'resource-card-title', item.id));
|
||||
headerMain.appendChild(element('div', 'resource-card-subtitle', tKey('stream_sessions.operation', { operation: item.operation_id })));
|
||||
|
||||
var pillRow = element('div', 'resource-pill-row');
|
||||
var statusPill = element('span', 'resource-status-pill ' + String(item.status || '').toLowerCase(), tKey('stream_sessions.status.' + item.status));
|
||||
pillRow.appendChild(statusPill);
|
||||
pillRow.appendChild(element('span', 'resource-status-pill', item.mode));
|
||||
pillRow.appendChild(element('span', 'resource-status-pill', item.protocol));
|
||||
headerMain.appendChild(pillRow);
|
||||
header.appendChild(headerMain);
|
||||
|
||||
var actions = element('div', 'resource-card-actions');
|
||||
if (item.status === 'created' || item.status === 'running') {
|
||||
var stopButton = element('button', 'btn-secondary', tKey('stream_sessions.stop'));
|
||||
stopButton.setAttribute('data-action', 'stop');
|
||||
stopButton.setAttribute('data-session-id', item.id);
|
||||
actions.appendChild(stopButton);
|
||||
}
|
||||
var toggleButton = element(
|
||||
'button',
|
||||
'btn-secondary',
|
||||
open ? tKey('stream_sessions.hide_details') : tKey('stream_sessions.show_details')
|
||||
);
|
||||
toggleButton.setAttribute('data-action', 'toggle');
|
||||
toggleButton.setAttribute('data-session-id', item.id);
|
||||
actions.appendChild(toggleButton);
|
||||
header.appendChild(actions);
|
||||
card.appendChild(header);
|
||||
|
||||
var metaGrid = element('div', 'resource-meta-grid');
|
||||
appendMetaItem(metaGrid, tKey('stream_sessions.meta.created'), formatDateTime(item.created_at));
|
||||
appendMetaItem(metaGrid, tKey('stream_sessions.meta.last_poll'), formatDateTime(item.last_poll_at));
|
||||
appendMetaItem(metaGrid, tKey('stream_sessions.meta.expires'), formatDateTime(item.expires_at));
|
||||
appendMetaItem(metaGrid, tKey('stream_sessions.meta.agent'), item.agent_id || '—');
|
||||
card.appendChild(metaGrid);
|
||||
|
||||
if (open) {
|
||||
appendDetailBlock(card, tKey('stream_sessions.cursor'), item.cursor);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
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.forEach(function(item) {
|
||||
list.appendChild(renderSessionCard(item));
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
@@ -38,14 +38,6 @@ 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;
|
||||
}
|
||||
@@ -91,14 +83,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -77,18 +77,6 @@ const BUNDLES = {
|
||||
'js/settings.js',
|
||||
],
|
||||
},
|
||||
'stream-sessions': {
|
||||
files: [
|
||||
'js/nav.js',
|
||||
'js/stream-sessions.js',
|
||||
],
|
||||
},
|
||||
'async-jobs': {
|
||||
files: [
|
||||
'js/nav.js',
|
||||
'js/async-jobs.js',
|
||||
],
|
||||
},
|
||||
'workspace-setup': {
|
||||
files: [
|
||||
'js/diagnostics.js',
|
||||
@@ -220,11 +208,9 @@ async function main() {
|
||||
rewriteHtml('html/login.html', manifest, '..');
|
||||
rewriteHtml('html/agents.html', manifest, '..');
|
||||
rewriteHtml('html/api-keys.html', manifest, '..');
|
||||
rewriteHtml('html/async-jobs.html', manifest, '..');
|
||||
rewriteHtml('html/logs.html', manifest, '..');
|
||||
rewriteHtml('html/secrets.html', manifest, '..');
|
||||
rewriteHtml('html/settings.html', manifest, '..');
|
||||
rewriteHtml('html/stream-sessions.html', manifest, '..');
|
||||
rewriteHtml('html/usage.html', manifest, '..');
|
||||
rewriteHtml('html/workspace-setup.html', manifest, '..');
|
||||
rewriteHtml('html/wizard/index.html', manifest, '../..');
|
||||
|
||||
@@ -58,20 +58,10 @@ cleanup() {
|
||||
kill "$(cat "$TMP_DIR/ui-server.pid")" >/dev/null 2>&1 || true
|
||||
rm -f "$TMP_DIR/ui-server.pid"
|
||||
fi
|
||||
if [[ -f "$TMP_DIR/stream-fixture.pid" ]]; then
|
||||
kill "$(cat "$TMP_DIR/stream-fixture.pid")" >/dev/null 2>&1 || true
|
||||
rm -f "$TMP_DIR/stream-fixture.pid"
|
||||
fi
|
||||
if [[ -f "$TMP_DIR/grpc-fixture.pid" ]]; then
|
||||
kill "$(cat "$TMP_DIR/grpc-fixture.pid")" >/dev/null 2>&1 || true
|
||||
rm -f "$TMP_DIR/grpc-fixture.pid"
|
||||
fi
|
||||
docker rm -f "$POSTGRES_CONTAINER" >/dev/null 2>&1 || true
|
||||
kill_port_processes "$UI_PORT"
|
||||
kill_port_processes "$ADMIN_PORT"
|
||||
kill_port_processes "$MCP_PORT"
|
||||
kill_port_processes "$STREAM_FIXTURE_PORT"
|
||||
kill_port_processes "${GRPC_FIXTURE_BIND##*:}"
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
@@ -128,9 +118,6 @@ export CRANK_BOOTSTRAP_ADMIN_PASSWORD="$ADMIN_PASSWORD"
|
||||
export CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME="Crank E2E"
|
||||
export CRANK_DEMO_SEED="true"
|
||||
export CRANK_BASE_URL="http://127.0.0.1:$UI_PORT"
|
||||
export CRANK_E2E_STREAM_FIXTURE_PORT="$STREAM_FIXTURE_PORT"
|
||||
export CRANK_E2E_GRPC_FIXTURE_BIND="$GRPC_FIXTURE_BIND"
|
||||
|
||||
mkdir -p "$CRANK_STORAGE_ROOT"
|
||||
|
||||
(
|
||||
@@ -153,24 +140,6 @@ until curl -fsS "http://127.0.0.1:$MCP_PORT/health" >/dev/null 2>&1; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
(
|
||||
cd "$ROOT_DIR/apps/ui"
|
||||
node scripts/stream-fixture-server.js >"$LOG_DIR/stream-fixture.log" 2>&1
|
||||
) &
|
||||
echo $! > "$TMP_DIR/stream-fixture.pid"
|
||||
|
||||
until curl -fsS "http://127.0.0.1:$STREAM_FIXTURE_PORT/health" >/dev/null 2>&1; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
(
|
||||
cd "$ROOT_DIR"
|
||||
cargo run -p crank-adapter-grpc --features test-support --example stream_fixture >"$LOG_DIR/grpc-fixture.log" 2>&1
|
||||
) &
|
||||
echo $! > "$TMP_DIR/grpc-fixture.pid"
|
||||
|
||||
wait_for_port "${GRPC_FIXTURE_BIND%%:*}" "${GRPC_FIXTURE_BIND##*:}"
|
||||
|
||||
(
|
||||
cd "$ROOT_DIR/apps/ui"
|
||||
node scripts/playwright-ui-server.js >"$LOG_DIR/ui-server.log" 2>&1
|
||||
|
||||
@@ -53,12 +53,6 @@ function mapRoute(urlPath) {
|
||||
if (urlPath === '/usage') {
|
||||
return path.join(ROOT_DIR, 'html', 'usage.html');
|
||||
}
|
||||
if (urlPath === '/stream-sessions') {
|
||||
return path.join(ROOT_DIR, 'html', 'stream-sessions.html');
|
||||
}
|
||||
if (urlPath === '/async-jobs') {
|
||||
return path.join(ROOT_DIR, 'html', 'async-jobs.html');
|
||||
}
|
||||
if (urlPath === '/settings') {
|
||||
return path.join(ROOT_DIR, 'html', 'settings.html');
|
||||
}
|
||||
@@ -171,14 +165,6 @@ const server = http.createServer((request, response) => {
|
||||
redirect(response, '/usage');
|
||||
return;
|
||||
}
|
||||
if (urlPath === '/html/stream-sessions.html') {
|
||||
redirect(response, '/stream-sessions');
|
||||
return;
|
||||
}
|
||||
if (urlPath === '/html/async-jobs.html') {
|
||||
redirect(response, '/async-jobs');
|
||||
return;
|
||||
}
|
||||
if (urlPath === '/html/settings.html') {
|
||||
redirect(response, '/settings');
|
||||
return;
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
const crypto = require('crypto');
|
||||
const http = require('http');
|
||||
|
||||
const PORT = Number(process.env.CRANK_E2E_STREAM_FIXTURE_PORT || 3310);
|
||||
|
||||
function writeJson(response, status, payload) {
|
||||
response.writeHead(status, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
response.end(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function readJsonBody(request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = '';
|
||||
request.setEncoding('utf8');
|
||||
request.on('data', (chunk) => {
|
||||
buffer += chunk;
|
||||
});
|
||||
request.on('end', () => {
|
||||
if (!buffer) {
|
||||
resolve({});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(buffer));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
request.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
const url = new URL(request.url, `http://127.0.0.1:${PORT}`);
|
||||
|
||||
if (url.pathname === '/health') {
|
||||
writeJson(response, 200, { ok: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/sse/logs') {
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream; charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
});
|
||||
|
||||
const events = [
|
||||
{ level: 'info', message: 'billing started', cursor: 'c1' },
|
||||
{ level: 'warn', message: 'cache warmup slow', cursor: 'c2' },
|
||||
{ level: 'error', message: 'invoice timeout', cursor: 'c3' },
|
||||
];
|
||||
|
||||
let index = 0;
|
||||
const timer = setInterval(() => {
|
||||
if (index >= events.length) {
|
||||
clearInterval(timer);
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
response.write('event: message\n');
|
||||
response.write(`data: ${JSON.stringify(events[index])}\n\n`);
|
||||
index += 1;
|
||||
}, 150);
|
||||
|
||||
request.on('close', () => {
|
||||
clearInterval(timer);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/snapshot/metrics') {
|
||||
writeJson(response, 200, {
|
||||
summary: {
|
||||
service: 'billing',
|
||||
error_rate: 0.12,
|
||||
},
|
||||
items: [
|
||||
{ timestamp: '2026-04-06T10:00:00Z', cpu: 0.41, memory: 0.67 },
|
||||
{ timestamp: '2026-04-06T10:00:05Z', cpu: 0.39, memory: 0.65 },
|
||||
],
|
||||
done: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/crm/leads' && request.method === 'POST') {
|
||||
readJsonBody(request)
|
||||
.then((payload) => {
|
||||
writeJson(response, 200, {
|
||||
id: 'lead_123',
|
||||
email: payload.email || null,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
writeJson(response, 400, { error: 'invalid_json' });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/soap/leads' && request.method === 'POST') {
|
||||
let buffer = '';
|
||||
request.setEncoding('utf8');
|
||||
request.on('data', (chunk) => {
|
||||
buffer += chunk;
|
||||
});
|
||||
request.on('end', () => {
|
||||
const hasEmail = buffer.includes('<email>user@example.com</email>')
|
||||
|| buffer.includes('<m:email>user@example.com</m:email>');
|
||||
|
||||
if (!hasEmail) {
|
||||
response.writeHead(400, {
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
response.end([
|
||||
'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">',
|
||||
'<soap:Body>',
|
||||
'<soap:Fault>',
|
||||
'<faultcode>Client</faultcode>',
|
||||
'<faultstring>missing email</faultstring>',
|
||||
'</soap:Fault>',
|
||||
'</soap:Body>',
|
||||
'</soap:Envelope>',
|
||||
].join(''));
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
response.end([
|
||||
'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">',
|
||||
'<soap:Body>',
|
||||
'<CreateLeadResponse>',
|
||||
'<id>lead_soap_123</id>',
|
||||
'<status>created</status>',
|
||||
'</CreateLeadResponse>',
|
||||
'</soap:Body>',
|
||||
'</soap:Envelope>',
|
||||
].join(''));
|
||||
});
|
||||
request.on('error', () => {
|
||||
response.writeHead(500, {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
response.end('fixture_error');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
writeJson(response, 404, { error: 'not_found' });
|
||||
});
|
||||
|
||||
function writeWebSocketFrame(socket, payload) {
|
||||
const body = Buffer.from(payload, 'utf8');
|
||||
const header = [];
|
||||
header.push(0x81);
|
||||
if (body.length < 126) {
|
||||
header.push(body.length);
|
||||
} else if (body.length < 65536) {
|
||||
header.push(126, (body.length >> 8) & 0xff, body.length & 0xff);
|
||||
} else {
|
||||
throw new Error('fixture payload is unexpectedly large');
|
||||
}
|
||||
|
||||
socket.write(Buffer.concat([Buffer.from(header), body]));
|
||||
}
|
||||
|
||||
function acceptWebSocket(request, socket) {
|
||||
const key = request.headers['sec-websocket-key'];
|
||||
if (!key) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const accept = crypto
|
||||
.createHash('sha1')
|
||||
.update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
|
||||
.digest('base64');
|
||||
|
||||
socket.write(
|
||||
[
|
||||
'HTTP/1.1 101 Switching Protocols',
|
||||
'Upgrade: websocket',
|
||||
'Connection: Upgrade',
|
||||
`Sec-WebSocket-Accept: ${accept}`,
|
||||
'\r\n',
|
||||
].join('\r\n'),
|
||||
);
|
||||
|
||||
const messages = [
|
||||
{ type: 'tick', seq: 1, value: 101 },
|
||||
{ type: 'tick', seq: 2, value: 102 },
|
||||
{ type: 'tick', seq: 3, value: 103 },
|
||||
];
|
||||
|
||||
let index = 0;
|
||||
const timer = setInterval(() => {
|
||||
if (index >= messages.length) {
|
||||
clearInterval(timer);
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
writeWebSocketFrame(socket, JSON.stringify(messages[index]));
|
||||
index += 1;
|
||||
}, 150);
|
||||
|
||||
socket.on('close', () => clearInterval(timer));
|
||||
socket.on('end', () => clearInterval(timer));
|
||||
socket.on('error', () => clearInterval(timer));
|
||||
}
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
if (request.url !== '/events') {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
if (head && head.length) {
|
||||
socket.unshift(head);
|
||||
}
|
||||
acceptWebSocket(request, socket);
|
||||
});
|
||||
|
||||
server.listen(PORT, '127.0.0.1', () => {
|
||||
console.log(`Streaming fixtures listening on http://127.0.0.1:${PORT}`);
|
||||
});
|
||||
@@ -10,8 +10,6 @@ const CORE_PAGES = [
|
||||
{ path: '/usage', heading: localized('Usage', 'Использование') },
|
||||
{ path: '/workspace-setup', heading: localized('Workspace', 'Воркспейс') },
|
||||
{ path: '/settings', heading: localized('Settings', 'Настройки') },
|
||||
{ path: '/stream-sessions', heading: localized('Stream Sessions', 'Stream Sessions') },
|
||||
{ path: '/async-jobs', heading: localized('Async Jobs', 'Async Jobs') },
|
||||
{ path: '/wizard/', heading: localized('Create', 'Создать') },
|
||||
];
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const {
|
||||
buildGrpcSessionOperationPayload,
|
||||
buildRestAsyncJobOperationPayload,
|
||||
buildRestWindowOperationPayload,
|
||||
initializeMcpSession,
|
||||
login,
|
||||
mcpToolCall,
|
||||
setupPublishedAgent,
|
||||
uniqueName,
|
||||
} = require('./helpers');
|
||||
|
||||
test('rest window tool executes through MCP', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
const operationName = uniqueName('playwright_window_logs');
|
||||
const agentSlug = uniqueName('sales_window');
|
||||
const payload = buildRestWindowOperationPayload(operationName);
|
||||
|
||||
const bundle = await setupPublishedAgent(page, {
|
||||
operationPayload: payload,
|
||||
agentSlug,
|
||||
toolName: operationName,
|
||||
toolTitle: 'Playwright Window Logs',
|
||||
toolDescription: 'Collects a bounded SSE log window from the local fixture.',
|
||||
});
|
||||
|
||||
const mcp = await initializeMcpSession({
|
||||
workspaceSlug: bundle.workspace.slug,
|
||||
agentSlug,
|
||||
apiKey: bundle.apiKeySecret,
|
||||
});
|
||||
const result = await mcpToolCall(mcp, operationName, {});
|
||||
|
||||
expect(result.window_complete).toBe(true);
|
||||
expect(result.has_more).toBe(false);
|
||||
expect(Array.isArray(result.items)).toBe(true);
|
||||
expect(result.items).toHaveLength(3);
|
||||
expect(result.items[0].message).toBe('billing started');
|
||||
expect(result.items[2].message).toBe('invoice timeout');
|
||||
});
|
||||
|
||||
test('grpc session tools create persisted stream sessions visible in UI', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
const operationName = uniqueName('playwright_echo_session');
|
||||
const agentSlug = uniqueName('sales_session');
|
||||
const payload = buildGrpcSessionOperationPayload(operationName);
|
||||
|
||||
const bundle = await setupPublishedAgent(page, {
|
||||
operationPayload: payload,
|
||||
agentSlug,
|
||||
toolName: operationName,
|
||||
toolTitle: 'Playwright gRPC Session',
|
||||
toolDescription: 'Streams gRPC echo messages through a bounded MCP session tool family.',
|
||||
});
|
||||
|
||||
const mcp = await initializeMcpSession({
|
||||
workspaceSlug: bundle.workspace.slug,
|
||||
agentSlug,
|
||||
apiKey: bundle.apiKeySecret,
|
||||
});
|
||||
|
||||
const started = await mcpToolCall(mcp, `${operationName}_start`, { message: 'hello' });
|
||||
const sessionId = started.session_id;
|
||||
expect(started.status).toBe('running');
|
||||
|
||||
const polled = await mcpToolCall(mcp, `${operationName}_poll`, { session_id: sessionId });
|
||||
expect(polled.session_id).toBe(sessionId);
|
||||
expect(Array.isArray(polled.items)).toBe(true);
|
||||
expect(polled.items[0].message).toContain('hello');
|
||||
|
||||
await page.goto('/stream-sessions');
|
||||
const sessionCard = page.locator(`.resource-card[data-session-id="${sessionId}"]`);
|
||||
await expect(sessionCard).toBeVisible();
|
||||
await sessionCard.locator('[data-action="toggle"]').click();
|
||||
await expect(sessionCard.locator('.resource-detail-pre')).toBeVisible();
|
||||
await sessionCard.locator('[data-action="stop"]').click();
|
||||
await expect(sessionCard.locator('[data-action="stop"]')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('async job tools complete and expose results in UI', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
const operationName = uniqueName('playwright_async_lead');
|
||||
const agentSlug = uniqueName('sales_async');
|
||||
const payload = buildRestAsyncJobOperationPayload(operationName);
|
||||
|
||||
const bundle = await setupPublishedAgent(page, {
|
||||
operationPayload: payload,
|
||||
agentSlug,
|
||||
toolName: operationName,
|
||||
toolTitle: 'Playwright Async Lead',
|
||||
toolDescription: 'Creates a lead through the async job MCP tool family.',
|
||||
});
|
||||
|
||||
const mcp = await initializeMcpSession({
|
||||
workspaceSlug: bundle.workspace.slug,
|
||||
agentSlug,
|
||||
apiKey: bundle.apiKeySecret,
|
||||
});
|
||||
|
||||
const started = await mcpToolCall(mcp, `${operationName}_start`, { email: 'user@example.com' });
|
||||
const jobId = started.job_id;
|
||||
expect(started.status).toBe('running');
|
||||
|
||||
let status = null;
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
status = await mcpToolCall(mcp, `${operationName}_status`, { job_id: jobId });
|
||||
if (status.status === 'completed') {
|
||||
break;
|
||||
}
|
||||
await page.waitForTimeout(50);
|
||||
}
|
||||
|
||||
expect(status.status).toBe('completed');
|
||||
const result = await mcpToolCall(mcp, `${operationName}_result`, { job_id: jobId });
|
||||
expect(result.id).toBe('lead_123');
|
||||
|
||||
await page.goto('/async-jobs');
|
||||
const jobCard = page.locator(`.resource-card[data-job-id="${jobId}"]`);
|
||||
await expect(jobCard).toBeVisible();
|
||||
await jobCard.locator('[data-action="toggle"]').click();
|
||||
await expect(page.locator(`[data-result-for="${jobId}"]`)).toContainText('lead_123');
|
||||
});
|
||||
Reference in New Issue
Block a user