feat(workbench): integrate operational workflows and map state

This commit is contained in:
2026-09-24 04:34:46 +08:00
parent 1752b9ef84
commit 436d5500f1
76 changed files with 5081 additions and 1937 deletions
+54 -2
View File
@@ -16,6 +16,8 @@ const emptyRuntimeSnapshot = {
work_orders: []
};
export const TEST_PROJECT_ID = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
export async function mockScadaApi(
page: Page,
devices: unknown[] = [],
@@ -25,13 +27,27 @@ export async function mockScadaApi(
analysisRuns: Record<string, unknown> = {},
analysisHistory: unknown[] = []
) {
const corsHeaders = { "Access-Control-Allow-Origin": "*" };
await page.route("https://tjwater-api.invalid/**", async (route) => {
if (route.request().method() === "OPTIONS") {
await route.fulfill({
status: 204,
headers: {
...corsHeaders,
"Access-Control-Allow-Headers": "Authorization, Content-Type, Last-Event-ID, X-Project-Id",
"Access-Control-Allow-Methods": "DELETE, GET, PATCH, POST, OPTIONS"
}
});
return;
}
const url = new URL(route.request().url());
if (url.pathname === "/api/v1/projects") {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify({
items: [{ project_id: "project-1", gs_workspace: "tjwater_next", status: "active" }],
items: [{ project_id: TEST_PROJECT_ID, gs_workspace: "tjwater_next", status: "active" }],
total: 1,
limit: 1000,
offset: 0
@@ -42,6 +58,7 @@ export async function mockScadaApi(
if (url.pathname === "/api/v1/scada-devices") {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify({ items: devices, total: devices.length, limit: 1000, offset: 0 })
});
return;
@@ -49,6 +66,7 @@ export async function mockScadaApi(
if (url.pathname === "/api/v1/projects/current/metadata" && projectPresentation) {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify(projectPresentation.metadata)
});
return;
@@ -56,6 +74,7 @@ export async function mockScadaApi(
if (url.pathname === "/api/v1/access-context" && projectPresentation) {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify(projectPresentation.accessContext)
});
return;
@@ -63,6 +82,7 @@ export async function mockScadaApi(
if (url.pathname === "/api/v1/timeseries/scada-readings") {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify(readings)
});
return;
@@ -70,16 +90,41 @@ export async function mockScadaApi(
if (url.pathname === "/api/v1/workbench/runtime") {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify(runtimeSnapshot)
});
return;
}
if (url.pathname === "/api/v1/workbench/sync-snapshot") {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify({
schema: "workbench-sync-snapshot@1",
event_cursor: 0,
runtime: runtimeSnapshot,
tasks: [],
notifications: [],
generated_at: "2026-09-17T02:30:00Z"
})
});
return;
}
if (url.pathname === "/api/v1/workbench/events") {
await route.fulfill({
contentType: "text/event-stream",
headers: { ...corsHeaders, "Cache-Control": "no-cache" },
body: ": test keepalive\n\n"
});
return;
}
if (url.pathname === "/api/v1/workbench/analysis-runs") {
const limit = Number(url.searchParams.get("limit") ?? 10);
const offset = Number(url.searchParams.get("offset") ?? 0);
const items = analysisHistory.slice(offset, offset + limit);
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify({
items,
limit,
@@ -96,17 +141,24 @@ export async function mockScadaApi(
if (analysisRun) {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify(analysisRun)
});
} else {
await route.fulfill({
status: 404,
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify({ detail: "Analysis run not found" })
});
}
return;
}
await route.fulfill({ status: 404, contentType: "application/json", body: "{}" });
await route.fulfill({
status: 404,
contentType: "application/json",
headers: corsHeaders,
body: "{}"
});
});
}