community: remove premium protocols and streaming surface

This commit is contained in:
a.tolmachev
2026-05-07 20:44:58 +00:00
parent 202c886793
commit 29f971c22d
78 changed files with 21 additions and 8838 deletions
+1 -18
View File
@@ -9,16 +9,9 @@ COPY apps/mcp-server/Cargo.toml apps/mcp-server/Cargo.toml
COPY crates/crank-core/Cargo.toml crates/crank-core/Cargo.toml
COPY crates/crank-schema/Cargo.toml crates/crank-schema/Cargo.toml
COPY crates/crank-mapping/Cargo.toml crates/crank-mapping/Cargo.toml
COPY crates/crank-proto/Cargo.toml crates/crank-proto/Cargo.toml
COPY crates/crank-registry/Cargo.toml crates/crank-registry/Cargo.toml
COPY crates/crank-runtime/Cargo.toml crates/crank-runtime/Cargo.toml
COPY crates/crank-adapter-rest/Cargo.toml crates/crank-adapter-rest/Cargo.toml
COPY crates/crank-adapter-graphql/Cargo.toml crates/crank-adapter-graphql/Cargo.toml
COPY crates/crank-adapter-grpc/Cargo.toml crates/crank-adapter-grpc/Cargo.toml
COPY crates/crank-adapter-grpc/build.rs crates/crank-adapter-grpc/build.rs
COPY crates/crank-adapter-grpc/proto crates/crank-adapter-grpc/proto
COPY crates/crank-adapter-websocket/Cargo.toml crates/crank-adapter-websocket/Cargo.toml
COPY crates/crank-adapter-soap/Cargo.toml crates/crank-adapter-soap/Cargo.toml
RUN mkdir -p \
apps/admin-api/src \
@@ -26,27 +19,17 @@ RUN mkdir -p \
crates/crank-core/src \
crates/crank-schema/src \
crates/crank-mapping/src \
crates/crank-proto/src \
crates/crank-registry/src \
crates/crank-runtime/src \
crates/crank-adapter-rest/src \
crates/crank-adapter-graphql/src \
crates/crank-adapter-grpc/src \
crates/crank-adapter-websocket/src \
crates/crank-adapter-soap/src \
&& printf 'fn main() {}\n' > apps/admin-api/src/main.rs \
&& printf 'fn main() {}\n' > apps/mcp-server/src/main.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-core/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-schema/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-mapping/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-proto/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-registry/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-runtime/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-adapter-rest/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-adapter-graphql/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-adapter-grpc/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-adapter-websocket/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-adapter-soap/src/lib.rs
&& printf 'pub fn placeholder() {}\n' > crates/crank-adapter-rest/src/lib.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git/db \
+2 -15
View File
@@ -26,6 +26,7 @@ use crate::{
capabilities::get_capabilities,
machine_auth::{issue_agent_token, issue_one_time_agent_token},
observability::{get_agent_usage, get_log, get_operation_usage, get_usage, list_logs},
streaming::list_protocol_capabilities,
operations::{
archive_operation, create_operation, create_version, delete_operation,
export_operation, generate_draft, get_operation, get_operation_version,
@@ -33,10 +34,6 @@ use crate::{
upload_output_json,
},
secrets::{create_secret, delete_secret, get_secret, list_secrets, rotate_secret},
streaming::{
cancel_async_job, get_async_job, get_async_job_result, get_stream_session,
list_async_jobs, list_protocol_capabilities, list_stream_sessions, stop_stream_session,
},
workspaces::{create_workspace, get_workspace, list_workspaces, update_workspace},
},
state::AppState,
@@ -131,17 +128,7 @@ pub fn build_app(state: AppState) -> Router {
.route("/usage", get(get_usage))
.route("/usage/operations/{operation_id}", get(get_operation_usage))
.route("/usage/agents/{agent_id}", get(get_agent_usage))
.route("/protocol-capabilities", get(list_protocol_capabilities))
.route("/stream-sessions", get(list_stream_sessions))
.route("/stream-sessions/{session_id}", get(get_stream_session))
.route(
"/stream-sessions/{session_id}/stop",
post(stop_stream_session),
)
.route("/async-jobs", get(list_async_jobs))
.route("/async-jobs/{job_id}", get(get_async_job))
.route("/async-jobs/{job_id}/cancel", post(cancel_async_job))
.route("/async-jobs/{job_id}/result", get(get_async_job_result));
.route("/protocol-capabilities", get(list_protocol_capabilities));
let workspace_root_router = Router::new()
.route("/capabilities", get(get_capabilities))
+1 -118
View File
@@ -1,28 +1,15 @@
use axum::{
Json,
extract::{Path, Query, State},
extract::{Path, State},
};
use serde_json::{Value, json};
use crate::{
error::ApiError,
routes::access::WorkspacePath,
service::{AsyncJobsQuery, StreamSessionsQuery},
state::AppState,
};
#[derive(serde::Deserialize)]
pub struct WorkspaceStreamSessionPath {
pub workspace_id: String,
pub session_id: String,
}
#[derive(serde::Deserialize)]
pub struct WorkspaceAsyncJobPath {
pub workspace_id: String,
pub job_id: String,
}
pub async fn list_protocol_capabilities(
Path(_path): Path<WorkspacePath>,
State(state): State<AppState>,
@@ -31,107 +18,3 @@ pub async fn list_protocol_capabilities(
"items": state.service.list_protocol_capabilities().await
})))
}
pub async fn list_stream_sessions(
Path(path): Path<WorkspacePath>,
Query(query): Query<StreamSessionsQuery>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let page = state
.service
.list_stream_sessions(&path.workspace_id.as_str().into(), query.clone())
.await?;
Ok(Json(json!({
"items": page.items,
"page": query.page.unwrap_or(1),
"page_size": query.page_size.unwrap_or(20),
"total": page.total,
})))
}
pub async fn get_stream_session(
Path(path): Path<WorkspaceStreamSessionPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let session = state
.service
.get_stream_session(
&path.workspace_id.as_str().into(),
&path.session_id.as_str().into(),
)
.await?;
Ok(Json(json!(session)))
}
pub async fn stop_stream_session(
Path(path): Path<WorkspaceStreamSessionPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let session = state
.service
.stop_stream_session(
&path.workspace_id.as_str().into(),
&path.session_id.as_str().into(),
)
.await?;
Ok(Json(json!(session)))
}
pub async fn list_async_jobs(
Path(path): Path<WorkspacePath>,
Query(query): Query<AsyncJobsQuery>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let page = state
.service
.list_async_jobs(&path.workspace_id.as_str().into(), query.clone())
.await?;
Ok(Json(json!({
"items": page.items,
"page": query.page.unwrap_or(1),
"page_size": query.page_size.unwrap_or(20),
"total": page.total,
})))
}
pub async fn get_async_job(
Path(path): Path<WorkspaceAsyncJobPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let job = state
.service
.get_async_job(
&path.workspace_id.as_str().into(),
&path.job_id.as_str().into(),
)
.await?;
Ok(Json(json!(job)))
}
pub async fn cancel_async_job(
Path(path): Path<WorkspaceAsyncJobPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let job = state
.service
.cancel_async_job(
&path.workspace_id.as_str().into(),
&path.job_id.as_str().into(),
)
.await?;
Ok(Json(json!(job)))
}
pub async fn get_async_job_result(
Path(path): Path<WorkspaceAsyncJobPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let result = state
.service
.get_async_job_result(
&path.workspace_id.as_str().into(),
&path.job_id.as_str().into(),
)
.await?;
Ok(Json(json!(result)))
}
-1
View File
@@ -26,7 +26,6 @@ tracing-subscriber.workspace = true
uuid.workspace = true
[dev-dependencies]
crank-adapter-grpc = { path = "../../crates/crank-adapter-grpc", features = ["test-support"] }
crank-mapping = { path = "../../crates/crank-mapping" }
crank-schema = { path = "../../crates/crank-schema" }
reqwest.workspace = true
+1 -18
View File
@@ -9,16 +9,9 @@ COPY apps/mcp-server/Cargo.toml apps/mcp-server/Cargo.toml
COPY crates/crank-core/Cargo.toml crates/crank-core/Cargo.toml
COPY crates/crank-schema/Cargo.toml crates/crank-schema/Cargo.toml
COPY crates/crank-mapping/Cargo.toml crates/crank-mapping/Cargo.toml
COPY crates/crank-proto/Cargo.toml crates/crank-proto/Cargo.toml
COPY crates/crank-registry/Cargo.toml crates/crank-registry/Cargo.toml
COPY crates/crank-runtime/Cargo.toml crates/crank-runtime/Cargo.toml
COPY crates/crank-adapter-rest/Cargo.toml crates/crank-adapter-rest/Cargo.toml
COPY crates/crank-adapter-graphql/Cargo.toml crates/crank-adapter-graphql/Cargo.toml
COPY crates/crank-adapter-grpc/Cargo.toml crates/crank-adapter-grpc/Cargo.toml
COPY crates/crank-adapter-grpc/build.rs crates/crank-adapter-grpc/build.rs
COPY crates/crank-adapter-grpc/proto crates/crank-adapter-grpc/proto
COPY crates/crank-adapter-websocket/Cargo.toml crates/crank-adapter-websocket/Cargo.toml
COPY crates/crank-adapter-soap/Cargo.toml crates/crank-adapter-soap/Cargo.toml
RUN mkdir -p \
apps/admin-api/src \
@@ -26,27 +19,17 @@ RUN mkdir -p \
crates/crank-core/src \
crates/crank-schema/src \
crates/crank-mapping/src \
crates/crank-proto/src \
crates/crank-registry/src \
crates/crank-runtime/src \
crates/crank-adapter-rest/src \
crates/crank-adapter-graphql/src \
crates/crank-adapter-grpc/src \
crates/crank-adapter-websocket/src \
crates/crank-adapter-soap/src \
&& printf 'fn main() {}\n' > apps/admin-api/src/main.rs \
&& printf 'fn main() {}\n' > apps/mcp-server/src/main.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-core/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-schema/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-mapping/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-proto/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-registry/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-runtime/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-adapter-rest/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-adapter-graphql/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-adapter-grpc/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-adapter-websocket/src/lib.rs \
&& printf 'pub fn placeholder() {}\n' > crates/crank-adapter-soap/src/lib.rs
&& printf 'pub fn placeholder() {}\n' > crates/crank-adapter-rest/src/lib.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git/db \
-106
View File
@@ -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>
-111
View File
@@ -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
View File
@@ -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>
-21
View File
@@ -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)
-232
View File
@@ -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();
});
-2
View File
@@ -9,8 +9,6 @@
secrets: '/secrets',
logs: '/logs',
usage: '/usage',
streamSessions: '/stream-sessions',
asyncJobs: '/async-jobs',
settings: '/settings',
workspaceSetup: '/workspace-setup',
wizard: '/wizard/'
-211
View File
@@ -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();
});
-16
View File
@@ -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;
}
-14
View File
@@ -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, '../..');
-31
View File
@@ -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
-14
View File
@@ -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;
-234
View File
@@ -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', 'Создать') },
];
-125
View File
@@ -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');
});