@blokjs/runner 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Blok.d.ts +19 -0
- package/dist/Blok.js +184 -0
- package/dist/Blok.js.map +1 -0
- package/dist/BlokResponse.d.ts +16 -0
- package/dist/BlokResponse.js +28 -0
- package/dist/BlokResponse.js.map +1 -0
- package/dist/Configuration.d.ts +37 -0
- package/dist/Configuration.js +248 -0
- package/dist/Configuration.js.map +1 -0
- package/dist/ConfigurationResolver.d.ts +7 -0
- package/dist/ConfigurationResolver.js +15 -0
- package/dist/ConfigurationResolver.js.map +1 -0
- package/dist/DefaultLogger.d.ts +65 -0
- package/dist/DefaultLogger.js +101 -0
- package/dist/DefaultLogger.js.map +1 -0
- package/dist/LocalStorage.d.ts +7 -0
- package/dist/LocalStorage.js +56 -0
- package/dist/LocalStorage.js.map +1 -0
- package/dist/MemoryUsage.d.ts +22 -0
- package/dist/MemoryUsage.js +83 -0
- package/dist/MemoryUsage.js.map +1 -0
- package/dist/NodeMap.d.ts +7 -0
- package/dist/NodeMap.js +13 -0
- package/dist/NodeMap.js.map +1 -0
- package/dist/ResolverBase.d.ts +8 -0
- package/dist/ResolverBase.js +18 -0
- package/dist/ResolverBase.js.map +1 -0
- package/dist/Runner.d.ts +25 -0
- package/dist/Runner.js +32 -0
- package/dist/Runner.js.map +1 -0
- package/dist/RunnerNode.d.ts +9 -0
- package/dist/RunnerNode.js +8 -0
- package/dist/RunnerNode.js.map +1 -0
- package/dist/RunnerNodeBase.d.ts +4 -0
- package/dist/RunnerNodeBase.js +3 -0
- package/dist/RunnerNodeBase.js.map +1 -0
- package/dist/RunnerSteps.d.ts +14 -0
- package/dist/RunnerSteps.js +110 -0
- package/dist/RunnerSteps.js.map +1 -0
- package/dist/RuntimeAdapterNode.d.ts +19 -0
- package/dist/RuntimeAdapterNode.js +87 -0
- package/dist/RuntimeAdapterNode.js.map +1 -0
- package/dist/RuntimeRegistry.d.ts +61 -0
- package/dist/RuntimeRegistry.js +87 -0
- package/dist/RuntimeRegistry.js.map +1 -0
- package/dist/TriggerBase.d.ts +119 -0
- package/dist/TriggerBase.js +413 -0
- package/dist/TriggerBase.js.map +1 -0
- package/dist/adapters/BunRuntimeAdapter.d.ts +38 -0
- package/dist/adapters/BunRuntimeAdapter.js +169 -0
- package/dist/adapters/BunRuntimeAdapter.js.map +1 -0
- package/dist/adapters/DockerRuntimeAdapter.d.ts +85 -0
- package/dist/adapters/DockerRuntimeAdapter.js +298 -0
- package/dist/adapters/DockerRuntimeAdapter.js.map +1 -0
- package/dist/adapters/HttpRuntimeAdapter.d.ts +58 -0
- package/dist/adapters/HttpRuntimeAdapter.js +152 -0
- package/dist/adapters/HttpRuntimeAdapter.js.map +1 -0
- package/dist/adapters/NodeJsRuntimeAdapter.d.ts +23 -0
- package/dist/adapters/NodeJsRuntimeAdapter.js +67 -0
- package/dist/adapters/NodeJsRuntimeAdapter.js.map +1 -0
- package/dist/adapters/RuntimeAdapter.d.ts +42 -0
- package/dist/adapters/RuntimeAdapter.js +2 -0
- package/dist/adapters/RuntimeAdapter.js.map +1 -0
- package/dist/adapters/WasmRuntimeAdapter.d.ts +69 -0
- package/dist/adapters/WasmRuntimeAdapter.js +279 -0
- package/dist/adapters/WasmRuntimeAdapter.js.map +1 -0
- package/dist/cache/NodeResultCache.d.ts +286 -0
- package/dist/cache/NodeResultCache.js +499 -0
- package/dist/cache/NodeResultCache.js.map +1 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +2 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cost/CostEstimator.d.ts +57 -0
- package/dist/cost/CostEstimator.js +171 -0
- package/dist/cost/CostEstimator.js.map +1 -0
- package/dist/cost/index.d.ts +4 -0
- package/dist/cost/index.js +3 -0
- package/dist/cost/index.js.map +1 -0
- package/dist/cost/pricing.d.ts +24 -0
- package/dist/cost/pricing.js +169 -0
- package/dist/cost/pricing.js.map +1 -0
- package/dist/defineNode.d.ts +155 -0
- package/dist/defineNode.js +191 -0
- package/dist/defineNode.js.map +1 -0
- package/dist/graphql/GraphQLSchemaGenerator.d.ts +129 -0
- package/dist/graphql/GraphQLSchemaGenerator.js +425 -0
- package/dist/graphql/GraphQLSchemaGenerator.js.map +1 -0
- package/dist/hmr/FileWatcher.d.ts +62 -0
- package/dist/hmr/FileWatcher.js +185 -0
- package/dist/hmr/FileWatcher.js.map +1 -0
- package/dist/hmr/HmrDevConsole.d.ts +13 -0
- package/dist/hmr/HmrDevConsole.js +46 -0
- package/dist/hmr/HmrDevConsole.js.map +1 -0
- package/dist/hmr/HotReloadManager.d.ts +84 -0
- package/dist/hmr/HotReloadManager.js +195 -0
- package/dist/hmr/HotReloadManager.js.map +1 -0
- package/dist/hmr/index.d.ts +39 -0
- package/dist/hmr/index.js +38 -0
- package/dist/hmr/index.js.map +1 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +107 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/APMIntegration.d.ts +141 -0
- package/dist/integrations/APMIntegration.js +212 -0
- package/dist/integrations/APMIntegration.js.map +1 -0
- package/dist/integrations/AzureMonitorIntegration.d.ts +118 -0
- package/dist/integrations/AzureMonitorIntegration.js +254 -0
- package/dist/integrations/AzureMonitorIntegration.js.map +1 -0
- package/dist/integrations/CloudWatchIntegration.d.ts +135 -0
- package/dist/integrations/CloudWatchIntegration.js +293 -0
- package/dist/integrations/CloudWatchIntegration.js.map +1 -0
- package/dist/integrations/SentryIntegration.d.ts +153 -0
- package/dist/integrations/SentryIntegration.js +200 -0
- package/dist/integrations/SentryIntegration.js.map +1 -0
- package/dist/integrations/index.d.ts +19 -0
- package/dist/integrations/index.js +16 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/marketplace/RuntimeAutoScaler.d.ts +148 -0
- package/dist/marketplace/RuntimeAutoScaler.js +366 -0
- package/dist/marketplace/RuntimeAutoScaler.js.map +1 -0
- package/dist/marketplace/RuntimeCatalog.d.ts +174 -0
- package/dist/marketplace/RuntimeCatalog.js +339 -0
- package/dist/marketplace/RuntimeCatalog.js.map +1 -0
- package/dist/marketplace/RuntimeDiscovery.d.ts +86 -0
- package/dist/marketplace/RuntimeDiscovery.js +219 -0
- package/dist/marketplace/RuntimeDiscovery.js.map +1 -0
- package/dist/marketplace/RuntimeHealthMonitor.d.ts +100 -0
- package/dist/marketplace/RuntimeHealthMonitor.js +241 -0
- package/dist/marketplace/RuntimeHealthMonitor.js.map +1 -0
- package/dist/marketplace/RuntimeMetricsDashboard.d.ts +113 -0
- package/dist/marketplace/RuntimeMetricsDashboard.js +293 -0
- package/dist/marketplace/RuntimeMetricsDashboard.js.map +1 -0
- package/dist/monitoring/CircuitBreaker.d.ts +107 -0
- package/dist/monitoring/CircuitBreaker.js +238 -0
- package/dist/monitoring/CircuitBreaker.js.map +1 -0
- package/dist/monitoring/DistributedTracer.d.ts +125 -0
- package/dist/monitoring/DistributedTracer.js +230 -0
- package/dist/monitoring/DistributedTracer.js.map +1 -0
- package/dist/monitoring/HealthCheck.d.ts +54 -0
- package/dist/monitoring/HealthCheck.js +102 -0
- package/dist/monitoring/HealthCheck.js.map +1 -0
- package/dist/monitoring/PerformanceProfiler.d.ts +63 -0
- package/dist/monitoring/PerformanceProfiler.js +229 -0
- package/dist/monitoring/PerformanceProfiler.js.map +1 -0
- package/dist/monitoring/PrometheusBootstrap.d.ts +30 -0
- package/dist/monitoring/PrometheusBootstrap.js +71 -0
- package/dist/monitoring/PrometheusBootstrap.js.map +1 -0
- package/dist/monitoring/PrometheusMetricsBridge.d.ts +60 -0
- package/dist/monitoring/PrometheusMetricsBridge.js +216 -0
- package/dist/monitoring/PrometheusMetricsBridge.js.map +1 -0
- package/dist/monitoring/RateLimiter.d.ts +58 -0
- package/dist/monitoring/RateLimiter.js +128 -0
- package/dist/monitoring/RateLimiter.js.map +1 -0
- package/dist/monitoring/StructuredLogger.d.ts +131 -0
- package/dist/monitoring/StructuredLogger.js +207 -0
- package/dist/monitoring/StructuredLogger.js.map +1 -0
- package/dist/monitoring/TracingBootstrap.d.ts +69 -0
- package/dist/monitoring/TracingBootstrap.js +129 -0
- package/dist/monitoring/TracingBootstrap.js.map +1 -0
- package/dist/monitoring/TriggerMetricsCollector.d.ts +94 -0
- package/dist/monitoring/TriggerMetricsCollector.js +174 -0
- package/dist/monitoring/TriggerMetricsCollector.js.map +1 -0
- package/dist/monitoring/index.d.ts +9 -0
- package/dist/monitoring/index.js +10 -0
- package/dist/monitoring/index.js.map +1 -0
- package/dist/openapi/OpenAPIGenerator.d.ts +192 -0
- package/dist/openapi/OpenAPIGenerator.js +373 -0
- package/dist/openapi/OpenAPIGenerator.js.map +1 -0
- package/dist/openapi/index.d.ts +20 -0
- package/dist/openapi/index.js +20 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/security/ABAC.d.ts +224 -0
- package/dist/security/ABAC.js +380 -0
- package/dist/security/ABAC.js.map +1 -0
- package/dist/security/AuditLogger.d.ts +242 -0
- package/dist/security/AuditLogger.js +317 -0
- package/dist/security/AuditLogger.js.map +1 -0
- package/dist/security/AuthMiddleware.d.ts +163 -0
- package/dist/security/AuthMiddleware.js +274 -0
- package/dist/security/AuthMiddleware.js.map +1 -0
- package/dist/security/EncryptionAtRest.d.ts +206 -0
- package/dist/security/EncryptionAtRest.js +236 -0
- package/dist/security/EncryptionAtRest.js.map +1 -0
- package/dist/security/OAuthProvider.d.ts +334 -0
- package/dist/security/OAuthProvider.js +719 -0
- package/dist/security/OAuthProvider.js.map +1 -0
- package/dist/security/PIIDetector.d.ts +233 -0
- package/dist/security/PIIDetector.js +354 -0
- package/dist/security/PIIDetector.js.map +1 -0
- package/dist/security/RBAC.d.ts +143 -0
- package/dist/security/RBAC.js +285 -0
- package/dist/security/RBAC.js.map +1 -0
- package/dist/security/SecretManager.d.ts +652 -0
- package/dist/security/SecretManager.js +1146 -0
- package/dist/security/SecretManager.js.map +1 -0
- package/dist/security/TLSConfig.d.ts +305 -0
- package/dist/security/TLSConfig.js +550 -0
- package/dist/security/TLSConfig.js.map +1 -0
- package/dist/security/index.d.ts +79 -0
- package/dist/security/index.js +80 -0
- package/dist/security/index.js.map +1 -0
- package/dist/testing/TestHarness.d.ts +189 -0
- package/dist/testing/TestHarness.js +272 -0
- package/dist/testing/TestHarness.js.map +1 -0
- package/dist/testing/TestLogger.d.ts +103 -0
- package/dist/testing/TestLogger.js +153 -0
- package/dist/testing/TestLogger.js.map +1 -0
- package/dist/testing/WorkflowTestRunner.d.ts +172 -0
- package/dist/testing/WorkflowTestRunner.js +355 -0
- package/dist/testing/WorkflowTestRunner.js.map +1 -0
- package/dist/testing/index.d.ts +21 -0
- package/dist/testing/index.js +22 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/tracing/InMemoryRunStore.d.ts +44 -0
- package/dist/tracing/InMemoryRunStore.js +341 -0
- package/dist/tracing/InMemoryRunStore.js.map +1 -0
- package/dist/tracing/PostgresRunStore.d.ts +82 -0
- package/dist/tracing/PostgresRunStore.js +640 -0
- package/dist/tracing/PostgresRunStore.js.map +1 -0
- package/dist/tracing/RunStore.d.ts +38 -0
- package/dist/tracing/RunStore.js +2 -0
- package/dist/tracing/RunStore.js.map +1 -0
- package/dist/tracing/RunTracker.d.ts +75 -0
- package/dist/tracing/RunTracker.js +374 -0
- package/dist/tracing/RunTracker.js.map +1 -0
- package/dist/tracing/SqliteRunStore.d.ts +53 -0
- package/dist/tracing/SqliteRunStore.js +703 -0
- package/dist/tracing/SqliteRunStore.js.map +1 -0
- package/dist/tracing/TraceRouter.d.ts +47 -0
- package/dist/tracing/TraceRouter.js +904 -0
- package/dist/tracing/TraceRouter.js.map +1 -0
- package/dist/tracing/TracingLogger.d.ts +21 -0
- package/dist/tracing/TracingLogger.js +62 -0
- package/dist/tracing/TracingLogger.js.map +1 -0
- package/dist/tracing/createStore.d.ts +30 -0
- package/dist/tracing/createStore.js +75 -0
- package/dist/tracing/createStore.js.map +1 -0
- package/dist/tracing/index.d.ts +13 -0
- package/dist/tracing/index.js +9 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/tracing/sanitize.d.ts +7 -0
- package/dist/tracing/sanitize.js +95 -0
- package/dist/tracing/sanitize.js.map +1 -0
- package/dist/tracing/types.d.ts +178 -0
- package/dist/tracing/types.js +3 -0
- package/dist/tracing/types.js.map +1 -0
- package/dist/types/Average.d.ts +11 -0
- package/dist/types/Average.js +2 -0
- package/dist/types/Average.js.map +1 -0
- package/dist/types/Condition.d.ts +8 -0
- package/dist/types/Condition.js +2 -0
- package/dist/types/Condition.js.map +1 -0
- package/dist/types/Conditions.d.ts +5 -0
- package/dist/types/Conditions.js +2 -0
- package/dist/types/Conditions.js.map +1 -0
- package/dist/types/Config.d.ts +12 -0
- package/dist/types/Config.js +2 -0
- package/dist/types/Config.js.map +1 -0
- package/dist/types/Flow.d.ts +5 -0
- package/dist/types/Flow.js +2 -0
- package/dist/types/Flow.js.map +1 -0
- package/dist/types/GlobalOptions.d.ts +11 -0
- package/dist/types/GlobalOptions.js +2 -0
- package/dist/types/GlobalOptions.js.map +1 -0
- package/dist/types/Inputs.d.ts +5 -0
- package/dist/types/Inputs.js +2 -0
- package/dist/types/Inputs.js.map +1 -0
- package/dist/types/JsonLikeObject.d.ts +3 -0
- package/dist/types/JsonLikeObject.js +2 -0
- package/dist/types/JsonLikeObject.js.map +1 -0
- package/dist/types/Mapper.d.ts +5 -0
- package/dist/types/Mapper.js +2 -0
- package/dist/types/Mapper.js.map +1 -0
- package/dist/types/Node.d.ts +10 -0
- package/dist/types/Node.js +2 -0
- package/dist/types/Node.js.map +1 -0
- package/dist/types/ParamsDictionary.d.ts +3 -0
- package/dist/types/ParamsDictionary.js +2 -0
- package/dist/types/ParamsDictionary.js.map +1 -0
- package/dist/types/Properties.d.ts +5 -0
- package/dist/types/Properties.js +2 -0
- package/dist/types/Properties.js.map +1 -0
- package/dist/types/Targets.d.ts +5 -0
- package/dist/types/Targets.js +2 -0
- package/dist/types/Targets.js.map +1 -0
- package/dist/types/Trigger.d.ts +5 -0
- package/dist/types/Trigger.js +2 -0
- package/dist/types/Trigger.js.map +1 -0
- package/dist/types/TriggerHttp.d.ts +7 -0
- package/dist/types/TriggerHttp.js +2 -0
- package/dist/types/TriggerHttp.js.map +1 -0
- package/dist/types/TriggerResponse.d.ts +6 -0
- package/dist/types/TriggerResponse.js +2 -0
- package/dist/types/TriggerResponse.js.map +1 -0
- package/dist/types/Triggers.d.ts +5 -0
- package/dist/types/Triggers.js +2 -0
- package/dist/types/Triggers.js.map +1 -0
- package/dist/types/TryCatch.d.ts +6 -0
- package/dist/types/TryCatch.js +2 -0
- package/dist/types/TryCatch.js.map +1 -0
- package/dist/visualization/NodeDependencyGraph.d.ts +76 -0
- package/dist/visualization/NodeDependencyGraph.js +418 -0
- package/dist/visualization/NodeDependencyGraph.js.map +1 -0
- package/dist/visualization/WorkflowVisualizer.d.ts +144 -0
- package/dist/visualization/WorkflowVisualizer.js +446 -0
- package/dist/visualization/WorkflowVisualizer.js.map +1 -0
- package/package.json +95 -0
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { RunTracker } from "./RunTracker";
|
|
3
|
+
/**
|
|
4
|
+
* Register trace API routes on an Express-compatible router.
|
|
5
|
+
*
|
|
6
|
+
* This function avoids importing express directly so the runner package
|
|
7
|
+
* doesn't need express as a dependency. The caller passes in a Router
|
|
8
|
+
* instance and the function registers all /__blok/* routes on it.
|
|
9
|
+
*
|
|
10
|
+
* Usage (in HttpTrigger.ts):
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { Router } from "express";
|
|
13
|
+
* import { registerTraceRoutes } from "@blokjs/runner";
|
|
14
|
+
* const traceRouter = Router();
|
|
15
|
+
* registerTraceRoutes(traceRouter);
|
|
16
|
+
* app.use("/__blok", traceRouter);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function registerTraceRoutes(router, tracker) {
|
|
20
|
+
const t = tracker || RunTracker.getInstance();
|
|
21
|
+
// --- CORS for cross-origin Studio UI ---
|
|
22
|
+
router.use((req, res, next) => {
|
|
23
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
24
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
25
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Last-Event-ID");
|
|
26
|
+
if (req.method === "OPTIONS") {
|
|
27
|
+
res.sendStatus(204);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
next();
|
|
31
|
+
});
|
|
32
|
+
// === Utility Endpoints ===
|
|
33
|
+
router.get("/health", (_req, res) => {
|
|
34
|
+
res.json({
|
|
35
|
+
status: "ok",
|
|
36
|
+
version: process.env.npm_package_version || "0.0.0",
|
|
37
|
+
uptime: process.uptime(),
|
|
38
|
+
activeRuns: t.getActiveRunCount(),
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
router.get("/config", (_req, res) => {
|
|
42
|
+
const summaries = t.getWorkflowSummaries();
|
|
43
|
+
const workflows = summaries.map((s) => s.name);
|
|
44
|
+
const triggers = [...new Set(summaries.flatMap((s) => s.triggerTypes))];
|
|
45
|
+
res.json({ workflows, triggers });
|
|
46
|
+
});
|
|
47
|
+
// === Workflow Endpoints ===
|
|
48
|
+
router.get("/workflows", (_req, res) => {
|
|
49
|
+
const summaries = t.getWorkflowSummaries();
|
|
50
|
+
res.json(summaries);
|
|
51
|
+
});
|
|
52
|
+
router.get("/workflows/:name", (req, res) => {
|
|
53
|
+
const { name } = req.params;
|
|
54
|
+
const summaries = t.getWorkflowSummaries();
|
|
55
|
+
const summary = summaries.find((s) => s.name === name);
|
|
56
|
+
if (!summary) {
|
|
57
|
+
res.status(404).json({ error: `Workflow '${name}' not found` });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Collect node names and runtimes from recent runs
|
|
61
|
+
const { runs } = t.getRuns({ workflow: name, limit: 10 });
|
|
62
|
+
const nodeNames = new Set();
|
|
63
|
+
const runtimes = new Set();
|
|
64
|
+
for (const run of runs) {
|
|
65
|
+
const nodes = t.getNodeRuns(run.id);
|
|
66
|
+
for (const node of nodes) {
|
|
67
|
+
nodeNames.add(node.nodeName);
|
|
68
|
+
if (node.runtimeKind)
|
|
69
|
+
runtimes.add(node.runtimeKind);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
res.json({
|
|
73
|
+
...summary,
|
|
74
|
+
nodeNames: Array.from(nodeNames),
|
|
75
|
+
runtimes: Array.from(runtimes),
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
router.get("/workflows/:name/runs", (req, res) => {
|
|
79
|
+
const { name } = req.params;
|
|
80
|
+
const status = req.query.status;
|
|
81
|
+
const limit = Number.parseInt(req.query.limit || "50", 10);
|
|
82
|
+
const offset = Number.parseInt(req.query.offset || "0", 10);
|
|
83
|
+
const sort = req.query.sort || "desc";
|
|
84
|
+
const result = t.getRuns({
|
|
85
|
+
workflow: name,
|
|
86
|
+
status: status,
|
|
87
|
+
limit,
|
|
88
|
+
offset,
|
|
89
|
+
sort,
|
|
90
|
+
});
|
|
91
|
+
res.json({
|
|
92
|
+
runs: result.runs,
|
|
93
|
+
total: result.total,
|
|
94
|
+
page: Math.floor(offset / limit) + 1,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
// === Diff (before /runs/:runId to avoid param collision) ===
|
|
98
|
+
/**
|
|
99
|
+
* Compare two runs side-by-side.
|
|
100
|
+
* Returns both runs with their nodes for diff view.
|
|
101
|
+
*/
|
|
102
|
+
router.get("/runs/diff", (req, res) => {
|
|
103
|
+
const runIdA = req.query.a;
|
|
104
|
+
const runIdB = req.query.b;
|
|
105
|
+
if (!runIdA || !runIdB) {
|
|
106
|
+
res.status(400).json({ error: "Both query params 'a' and 'b' are required" });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const runA = t.getRun(runIdA);
|
|
110
|
+
const runB = t.getRun(runIdB);
|
|
111
|
+
if (!runA) {
|
|
112
|
+
res.status(404).json({ error: `Run '${runIdA}' not found` });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!runB) {
|
|
116
|
+
res.status(404).json({ error: `Run '${runIdB}' not found` });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
res.json({
|
|
120
|
+
runA: { run: runA, nodes: t.getNodeRuns(runIdA), logs: t.getLogs(runIdA) },
|
|
121
|
+
runB: { run: runB, nodes: t.getNodeRuns(runIdB), logs: t.getLogs(runIdB) },
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
// === Tags ===
|
|
125
|
+
router.get("/tags", (_req, res) => {
|
|
126
|
+
res.json({ tags: t.getAllTags() });
|
|
127
|
+
});
|
|
128
|
+
router.post("/runs/:runId/tags", (req, res) => {
|
|
129
|
+
const { runId } = req.params;
|
|
130
|
+
const run = t.getRun(runId);
|
|
131
|
+
if (!run) {
|
|
132
|
+
res.status(404).json({ error: `Run '${runId}' not found` });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const body = req.body;
|
|
136
|
+
const tagsToAdd = [];
|
|
137
|
+
if (body?.tag)
|
|
138
|
+
tagsToAdd.push(body.tag);
|
|
139
|
+
if (body?.tags)
|
|
140
|
+
tagsToAdd.push(...body.tags);
|
|
141
|
+
if (tagsToAdd.length === 0) {
|
|
142
|
+
res.status(400).json({ error: "Provide 'tag' or 'tags' in request body" });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const added = [];
|
|
146
|
+
for (const tag of tagsToAdd) {
|
|
147
|
+
if (t.addTag(runId, tag.trim())) {
|
|
148
|
+
added.push(tag.trim());
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
res.json({ added, tags: run.tags || [] });
|
|
152
|
+
});
|
|
153
|
+
router.delete("/runs/:runId/tags/:tag", (req, res) => {
|
|
154
|
+
const { runId, tag } = req.params;
|
|
155
|
+
const run = t.getRun(runId);
|
|
156
|
+
if (!run) {
|
|
157
|
+
res.status(404).json({ error: `Run '${runId}' not found` });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const removed = t.removeTag(runId, tag);
|
|
161
|
+
res.json({ removed, tags: run.tags || [] });
|
|
162
|
+
});
|
|
163
|
+
// === Metrics ===
|
|
164
|
+
router.get("/metrics", (req, res) => {
|
|
165
|
+
const workflow = req.query.workflow;
|
|
166
|
+
const metrics = t.getMetrics(workflow);
|
|
167
|
+
res.json(metrics);
|
|
168
|
+
});
|
|
169
|
+
// === Export ===
|
|
170
|
+
/**
|
|
171
|
+
* Export runs as JSON or CSV.
|
|
172
|
+
* Bulk export: GET /__blok/runs/export?format=json|csv&workflow=...&status=...&limit=1000
|
|
173
|
+
* Must be registered before /runs/:runId to avoid param collision.
|
|
174
|
+
*/
|
|
175
|
+
router.get("/runs/export", (req, res) => {
|
|
176
|
+
const format = (req.query.format || "json");
|
|
177
|
+
const workflow = req.query.workflow;
|
|
178
|
+
const status = req.query.status;
|
|
179
|
+
const limit = Number.parseInt(req.query.limit || "1000", 10);
|
|
180
|
+
const result = t.getRuns({
|
|
181
|
+
workflow,
|
|
182
|
+
status: status,
|
|
183
|
+
limit,
|
|
184
|
+
sort: "desc",
|
|
185
|
+
});
|
|
186
|
+
if (format === "csv") {
|
|
187
|
+
const csv = runsToCsv(result.runs);
|
|
188
|
+
res.setHeader("Content-Type", "text/csv");
|
|
189
|
+
res.setHeader("Content-Disposition", `attachment; filename="blok-runs-${Date.now()}.csv"`);
|
|
190
|
+
res.write(csv);
|
|
191
|
+
res.end();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// JSON export — include full detail for each run
|
|
195
|
+
const exportData = {
|
|
196
|
+
exportedAt: new Date().toISOString(),
|
|
197
|
+
format: "json",
|
|
198
|
+
total: result.runs.length,
|
|
199
|
+
runs: result.runs.map((run) => ({
|
|
200
|
+
run,
|
|
201
|
+
nodes: t.getNodeRuns(run.id),
|
|
202
|
+
events: t.getEvents(run.id),
|
|
203
|
+
logs: t.getLogs(run.id),
|
|
204
|
+
})),
|
|
205
|
+
};
|
|
206
|
+
res.setHeader("Content-Type", "application/json");
|
|
207
|
+
res.setHeader("Content-Disposition", `attachment; filename="blok-runs-${Date.now()}.json"`);
|
|
208
|
+
res.json(exportData);
|
|
209
|
+
});
|
|
210
|
+
/**
|
|
211
|
+
* Export a single run as JSON or CSV.
|
|
212
|
+
* GET /__blok/runs/:runId/export?format=json|csv
|
|
213
|
+
*/
|
|
214
|
+
router.get("/runs/:runId/export", (req, res) => {
|
|
215
|
+
const { runId } = req.params;
|
|
216
|
+
const format = (req.query.format || "json");
|
|
217
|
+
const run = t.getRun(runId);
|
|
218
|
+
if (!run) {
|
|
219
|
+
res.status(404).json({ error: `Run '${runId}' not found` });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const nodes = t.getNodeRuns(runId);
|
|
223
|
+
const events = t.getEvents(runId);
|
|
224
|
+
const logs = t.getLogs(runId);
|
|
225
|
+
if (format === "csv") {
|
|
226
|
+
const csv = singleRunToCsv(run, nodes, logs);
|
|
227
|
+
res.setHeader("Content-Type", "text/csv");
|
|
228
|
+
res.setHeader("Content-Disposition", `attachment; filename="blok-run-${runId}.csv"`);
|
|
229
|
+
res.write(csv);
|
|
230
|
+
res.end();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const exportData = {
|
|
234
|
+
exportedAt: new Date().toISOString(),
|
|
235
|
+
format: "json",
|
|
236
|
+
run,
|
|
237
|
+
nodes,
|
|
238
|
+
events,
|
|
239
|
+
logs,
|
|
240
|
+
};
|
|
241
|
+
res.setHeader("Content-Type", "application/json");
|
|
242
|
+
res.setHeader("Content-Disposition", `attachment; filename="blok-run-${runId}.json"`);
|
|
243
|
+
res.json(exportData);
|
|
244
|
+
});
|
|
245
|
+
// === Webhooks ===
|
|
246
|
+
/**
|
|
247
|
+
* List registered webhooks.
|
|
248
|
+
*/
|
|
249
|
+
router.get("/webhooks", (_req, res) => {
|
|
250
|
+
res.json({ webhooks: t.getWebhooks() });
|
|
251
|
+
});
|
|
252
|
+
/**
|
|
253
|
+
* Register a webhook.
|
|
254
|
+
* Body: { url: string, events?: string[], secret?: string }
|
|
255
|
+
*/
|
|
256
|
+
router.post("/webhooks", (req, res) => {
|
|
257
|
+
const body = (req.body || {});
|
|
258
|
+
if (!body.url) {
|
|
259
|
+
res.status(400).json({ error: "Missing required field 'url'" });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
new URL(body.url);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
res.status(400).json({ error: "Invalid URL" });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const webhook = t.registerWebhook({
|
|
270
|
+
url: body.url,
|
|
271
|
+
events: body.events || ["run.completed", "run.failed"],
|
|
272
|
+
secret: body.secret,
|
|
273
|
+
});
|
|
274
|
+
res.status(201).json(webhook);
|
|
275
|
+
});
|
|
276
|
+
/**
|
|
277
|
+
* Remove a webhook.
|
|
278
|
+
*/
|
|
279
|
+
router.delete("/webhooks/:id", (req, res) => {
|
|
280
|
+
const { id } = req.params;
|
|
281
|
+
const removed = t.removeWebhook(id);
|
|
282
|
+
if (!removed) {
|
|
283
|
+
res.status(404).json({ error: `Webhook '${id}' not found` });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
res.json({ removed: true });
|
|
287
|
+
});
|
|
288
|
+
// === Run Endpoints ===
|
|
289
|
+
router.get("/runs", (req, res) => {
|
|
290
|
+
const workflow = req.query.workflow;
|
|
291
|
+
const status = req.query.status;
|
|
292
|
+
const tags = req.query.tags ? req.query.tags.split(",").map((t) => t.trim()) : undefined;
|
|
293
|
+
const limit = Number.parseInt(req.query.limit || "50", 10);
|
|
294
|
+
const offset = Number.parseInt(req.query.offset || "0", 10);
|
|
295
|
+
const sort = req.query.sort || "desc";
|
|
296
|
+
const result = t.getRuns({
|
|
297
|
+
workflow,
|
|
298
|
+
status: status,
|
|
299
|
+
tags,
|
|
300
|
+
limit,
|
|
301
|
+
offset,
|
|
302
|
+
sort,
|
|
303
|
+
});
|
|
304
|
+
res.json({
|
|
305
|
+
runs: result.runs,
|
|
306
|
+
total: result.total,
|
|
307
|
+
page: Math.floor(offset / limit) + 1,
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
router.get("/runs/:runId", (req, res) => {
|
|
311
|
+
const { runId } = req.params;
|
|
312
|
+
const run = t.getRun(runId);
|
|
313
|
+
if (!run) {
|
|
314
|
+
res.status(404).json({ error: `Run '${runId}' not found` });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const nodes = t.getNodeRuns(runId);
|
|
318
|
+
const logs = t.getLogs(runId);
|
|
319
|
+
res.json({ run, nodes, logs });
|
|
320
|
+
});
|
|
321
|
+
router.get("/runs/:runId/events", (req, res) => {
|
|
322
|
+
const { runId } = req.params;
|
|
323
|
+
const since = req.query.since ? Number.parseInt(req.query.since, 10) : undefined;
|
|
324
|
+
const run = t.getRun(runId);
|
|
325
|
+
if (!run) {
|
|
326
|
+
res.status(404).json({ error: `Run '${runId}' not found` });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const events = t.getEvents(runId, since);
|
|
330
|
+
res.json(events);
|
|
331
|
+
});
|
|
332
|
+
router.delete("/runs", (_req, res) => {
|
|
333
|
+
const deleted = t.clearAll();
|
|
334
|
+
res.json({ deleted });
|
|
335
|
+
});
|
|
336
|
+
// === Replay ===
|
|
337
|
+
/**
|
|
338
|
+
* Re-trigger a workflow by replaying a previous run.
|
|
339
|
+
* Makes an HTTP request to the original workflow endpoint.
|
|
340
|
+
*/
|
|
341
|
+
router.post("/runs/:runId/replay", (req, res) => {
|
|
342
|
+
const { runId } = req.params;
|
|
343
|
+
const run = t.getRun(runId);
|
|
344
|
+
if (!run) {
|
|
345
|
+
res.status(404).json({ error: `Run '${runId}' not found` });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (run.triggerType !== "http") {
|
|
349
|
+
res.status(400).json({ error: `Replay is only supported for HTTP triggers (got '${run.triggerType}')` });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Parse method and path from triggerSummary (e.g. "GET /countries")
|
|
353
|
+
const parts = run.triggerSummary.split(" ");
|
|
354
|
+
const method = (parts[0] || "GET").toUpperCase();
|
|
355
|
+
const path = parts[1] || "/";
|
|
356
|
+
// Determine the host to call (use the incoming request's Host header)
|
|
357
|
+
const host = req.headers.host || "localhost:4000";
|
|
358
|
+
const protocol = "http";
|
|
359
|
+
const url = `${protocol}://${host}${path}`;
|
|
360
|
+
// Allow overriding method, path, headers, and body via request body
|
|
361
|
+
const overrides = (req.body || {});
|
|
362
|
+
const finalMethod = (overrides.method || method).toUpperCase();
|
|
363
|
+
const finalUrl = overrides.path ? `${protocol}://${host}${overrides.path}` : url;
|
|
364
|
+
const customHeaders = {
|
|
365
|
+
"Content-Type": "application/json",
|
|
366
|
+
...(overrides.headers || {}),
|
|
367
|
+
};
|
|
368
|
+
const body = overrides.body !== undefined ? JSON.stringify(overrides.body) : undefined;
|
|
369
|
+
// Listen for the next RUN_STARTED event matching this workflow
|
|
370
|
+
const timeout = setTimeout(() => {
|
|
371
|
+
cleanup();
|
|
372
|
+
res.status(504).json({ error: "Replay timed out waiting for new run" });
|
|
373
|
+
}, 10000);
|
|
374
|
+
const cleanup = () => {
|
|
375
|
+
clearTimeout(timeout);
|
|
376
|
+
t.removeListener("RUN_STARTED", onRunStarted);
|
|
377
|
+
};
|
|
378
|
+
const onRunStarted = (event) => {
|
|
379
|
+
if (event.workflowName !== run.workflowName)
|
|
380
|
+
return;
|
|
381
|
+
cleanup();
|
|
382
|
+
res.json({
|
|
383
|
+
newRunId: event.runId,
|
|
384
|
+
originalRunId: runId,
|
|
385
|
+
workflowName: run.workflowName,
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
t.on("RUN_STARTED", onRunStarted);
|
|
389
|
+
// Make the HTTP request to re-trigger the workflow
|
|
390
|
+
const parsedUrl = new URL(finalUrl);
|
|
391
|
+
const reqOpts = {
|
|
392
|
+
hostname: parsedUrl.hostname,
|
|
393
|
+
port: parsedUrl.port,
|
|
394
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
395
|
+
method: finalMethod,
|
|
396
|
+
headers: customHeaders,
|
|
397
|
+
};
|
|
398
|
+
const httpReq = http.request(reqOpts, (httpRes) => {
|
|
399
|
+
// Consume response body to prevent memory leaks
|
|
400
|
+
const chunks = [];
|
|
401
|
+
httpRes.on("data", (chunk) => chunks.push(chunk));
|
|
402
|
+
httpRes.on("end", () => {
|
|
403
|
+
// If we haven't already responded (via onRunStarted), respond now
|
|
404
|
+
// The RUN_STARTED listener should have fired before the response ends
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
httpReq.on("error", (err) => {
|
|
408
|
+
cleanup();
|
|
409
|
+
res.status(502).json({ error: `Replay request failed: ${err.message}` });
|
|
410
|
+
});
|
|
411
|
+
if (body) {
|
|
412
|
+
httpReq.write(body);
|
|
413
|
+
}
|
|
414
|
+
httpReq.end();
|
|
415
|
+
// Cleanup if client disconnects
|
|
416
|
+
req.on("close", cleanup);
|
|
417
|
+
});
|
|
418
|
+
// === AI Error Explanation ===
|
|
419
|
+
/**
|
|
420
|
+
* Explain a run or node error using an LLM.
|
|
421
|
+
* Requires OPENAI_API_KEY environment variable.
|
|
422
|
+
*
|
|
423
|
+
* POST /__blok/runs/:runId/explain
|
|
424
|
+
* Body: { nodeId?: string }
|
|
425
|
+
* Returns: { explanation: string, model: string }
|
|
426
|
+
*/
|
|
427
|
+
router.post("/runs/:runId/explain", async (req, res) => {
|
|
428
|
+
const { runId } = req.params;
|
|
429
|
+
const run = t.getRun(runId);
|
|
430
|
+
if (!run) {
|
|
431
|
+
res.status(404).json({ error: `Run '${runId}' not found` });
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
435
|
+
if (!apiKey) {
|
|
436
|
+
res.status(503).json({
|
|
437
|
+
error: "AI explanation unavailable — set OPENAI_API_KEY environment variable",
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const body = (req.body || {});
|
|
442
|
+
const nodes = t.getNodeRuns(runId);
|
|
443
|
+
const logs = t.getLogs(runId);
|
|
444
|
+
// Build context for the LLM
|
|
445
|
+
let errorContext;
|
|
446
|
+
if (body.nodeId) {
|
|
447
|
+
const node = nodes.find((n) => n.id === body.nodeId);
|
|
448
|
+
if (!node) {
|
|
449
|
+
res.status(404).json({ error: `Node '${body.nodeId}' not found in run` });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (!node.error) {
|
|
453
|
+
res.status(400).json({ error: `Node '${node.nodeName}' has no error` });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const nodeLogs = logs.filter((l) => l.nodeId === node.id || l.nodeName === node.nodeName);
|
|
457
|
+
errorContext = buildNodeErrorContext(run, node, nodes, nodeLogs);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
if (!run.error) {
|
|
461
|
+
res.status(400).json({ error: "This run has no error to explain" });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const failedNodes = nodes.filter((n) => n.status === "failed");
|
|
465
|
+
errorContext = buildRunErrorContext(run, nodes, failedNodes, logs);
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
const model = process.env.BLOK_AI_MODEL || "gpt-4o-mini";
|
|
469
|
+
const explanation = await callOpenAI(apiKey, model, errorContext);
|
|
470
|
+
res.json({ explanation, model });
|
|
471
|
+
}
|
|
472
|
+
catch (err) {
|
|
473
|
+
const msg = err instanceof Error ? err.message : "Unknown AI API error";
|
|
474
|
+
res.status(502).json({ error: `AI explanation failed: ${msg}` });
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
// === Search ===
|
|
478
|
+
/**
|
|
479
|
+
* Search across workflows and runs.
|
|
480
|
+
* Used by the command palette (Cmd+K).
|
|
481
|
+
*/
|
|
482
|
+
router.get("/search", (req, res) => {
|
|
483
|
+
const query = (req.query.q || "").toLowerCase().trim();
|
|
484
|
+
if (!query) {
|
|
485
|
+
res.json({ workflows: [], runs: [] });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
// Search workflows
|
|
489
|
+
const allWorkflows = t.getWorkflowSummaries();
|
|
490
|
+
const matchedWorkflows = allWorkflows.filter((w) => w.name.toLowerCase().includes(query) ||
|
|
491
|
+
w.path.toLowerCase().includes(query) ||
|
|
492
|
+
w.triggerTypes.some((tt) => tt.toLowerCase().includes(query)));
|
|
493
|
+
// Search runs (by ID, workflow name, trigger summary, or error message)
|
|
494
|
+
const { runs: allRuns } = t.getRuns({ limit: 200 });
|
|
495
|
+
const matchedRuns = allRuns
|
|
496
|
+
.filter((r) => r.id.toLowerCase().includes(query) ||
|
|
497
|
+
r.workflowName.toLowerCase().includes(query) ||
|
|
498
|
+
r.triggerSummary.toLowerCase().includes(query) ||
|
|
499
|
+
r.error?.message.toLowerCase().includes(query) ||
|
|
500
|
+
r.status.toLowerCase().includes(query))
|
|
501
|
+
.slice(0, 20);
|
|
502
|
+
res.json({
|
|
503
|
+
workflows: matchedWorkflows.slice(0, 10),
|
|
504
|
+
runs: matchedRuns,
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
// === Custom Dashboards ===
|
|
508
|
+
/**
|
|
509
|
+
* List all dashboards.
|
|
510
|
+
* GET /__blok/dashboards
|
|
511
|
+
*/
|
|
512
|
+
router.get("/dashboards", (_req, res) => {
|
|
513
|
+
const dashboards = t.listDashboards();
|
|
514
|
+
res.json({ dashboards });
|
|
515
|
+
});
|
|
516
|
+
/**
|
|
517
|
+
* Get a single dashboard by ID.
|
|
518
|
+
* GET /__blok/dashboards/:dashboardId
|
|
519
|
+
*/
|
|
520
|
+
router.get("/dashboards/:dashboardId", (req, res) => {
|
|
521
|
+
const dashboard = t.getDashboard(req.params.dashboardId);
|
|
522
|
+
if (!dashboard) {
|
|
523
|
+
res.status(404).json({ error: `Dashboard '${req.params.dashboardId}' not found` });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
res.json(dashboard);
|
|
527
|
+
});
|
|
528
|
+
/**
|
|
529
|
+
* Create a new dashboard.
|
|
530
|
+
* POST /__blok/dashboards
|
|
531
|
+
* Body: { name, description?, widgets?, isDefault? }
|
|
532
|
+
*/
|
|
533
|
+
router.post("/dashboards", (req, res) => {
|
|
534
|
+
const body = (req.body || {});
|
|
535
|
+
if (!body.name || typeof body.name !== "string" || body.name.trim().length === 0) {
|
|
536
|
+
res.status(400).json({ error: "Dashboard name is required" });
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const now = Date.now();
|
|
540
|
+
const dashboard = {
|
|
541
|
+
id: `dash_${now.toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
542
|
+
name: body.name.trim(),
|
|
543
|
+
description: body.description,
|
|
544
|
+
isDefault: body.isDefault ?? false,
|
|
545
|
+
createdAt: now,
|
|
546
|
+
updatedAt: now,
|
|
547
|
+
widgets: Array.isArray(body.widgets) ? body.widgets : [],
|
|
548
|
+
};
|
|
549
|
+
t.saveDashboard(dashboard);
|
|
550
|
+
res.status(201).json(dashboard);
|
|
551
|
+
});
|
|
552
|
+
/**
|
|
553
|
+
* Update an existing dashboard.
|
|
554
|
+
* PUT /__blok/dashboards/:dashboardId
|
|
555
|
+
* Body: { name?, description?, widgets?, isDefault? }
|
|
556
|
+
*/
|
|
557
|
+
router.put("/dashboards/:dashboardId", (req, res) => {
|
|
558
|
+
const { dashboardId } = req.params;
|
|
559
|
+
const existing = t.getDashboard(dashboardId);
|
|
560
|
+
if (!existing) {
|
|
561
|
+
res.status(404).json({ error: `Dashboard '${dashboardId}' not found` });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const body = (req.body || {});
|
|
565
|
+
t.updateDashboard(dashboardId, body);
|
|
566
|
+
const updated = t.getDashboard(dashboardId);
|
|
567
|
+
res.json(updated);
|
|
568
|
+
});
|
|
569
|
+
/**
|
|
570
|
+
* Delete a dashboard.
|
|
571
|
+
* DELETE /__blok/dashboards/:dashboardId
|
|
572
|
+
*/
|
|
573
|
+
router.delete("/dashboards/:dashboardId", (req, res) => {
|
|
574
|
+
const deleted = t.deleteDashboard(req.params.dashboardId);
|
|
575
|
+
if (!deleted) {
|
|
576
|
+
res.status(404).json({ error: `Dashboard '${req.params.dashboardId}' not found` });
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
res.json({ deleted: true });
|
|
580
|
+
});
|
|
581
|
+
/**
|
|
582
|
+
* Duplicate a dashboard.
|
|
583
|
+
* POST /__blok/dashboards/:dashboardId/duplicate
|
|
584
|
+
*/
|
|
585
|
+
router.post("/dashboards/:dashboardId/duplicate", (req, res) => {
|
|
586
|
+
const source = t.getDashboard(req.params.dashboardId);
|
|
587
|
+
if (!source) {
|
|
588
|
+
res.status(404).json({ error: `Dashboard '${req.params.dashboardId}' not found` });
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const now = Date.now();
|
|
592
|
+
const copy = {
|
|
593
|
+
...source,
|
|
594
|
+
id: `dash_${now.toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
595
|
+
name: `${source.name} (Copy)`,
|
|
596
|
+
isDefault: false,
|
|
597
|
+
createdAt: now,
|
|
598
|
+
updatedAt: now,
|
|
599
|
+
};
|
|
600
|
+
t.saveDashboard(copy);
|
|
601
|
+
res.status(201).json(copy);
|
|
602
|
+
});
|
|
603
|
+
// === SSE Endpoints ===
|
|
604
|
+
/**
|
|
605
|
+
* SSE stream for a specific run.
|
|
606
|
+
* Sends all past events as a replay, then streams new events live.
|
|
607
|
+
* Auto-closes when the run finishes.
|
|
608
|
+
*/
|
|
609
|
+
router.get("/runs/:runId/stream", (req, res) => {
|
|
610
|
+
const { runId } = req.params;
|
|
611
|
+
const run = t.getRun(runId);
|
|
612
|
+
if (!run) {
|
|
613
|
+
res.status(404).json({ error: `Run '${runId}' not found` });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
// Set SSE headers
|
|
617
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
618
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
619
|
+
res.setHeader("Connection", "keep-alive");
|
|
620
|
+
res.setHeader("X-Accel-Buffering", "no"); // Disable nginx buffering
|
|
621
|
+
res.flushHeaders();
|
|
622
|
+
// Immediate acknowledgment so the browser fires onopen without waiting
|
|
623
|
+
res.write(`event: connected\ndata: ${JSON.stringify({ runId, timestamp: Date.now() })}\n\n`);
|
|
624
|
+
res.write("retry: 3000\n\n");
|
|
625
|
+
// Replay past events (respecting Last-Event-ID for reconnection).
|
|
626
|
+
// Cap fresh connections to last 50 events to avoid blocking the stream.
|
|
627
|
+
// The client fetches full run state via GET /runs/:runId.
|
|
628
|
+
const MAX_REPLAY_EVENTS = 50;
|
|
629
|
+
const lastEventId = req.headers["last-event-id"];
|
|
630
|
+
const existingEvents = t.getEvents(runId);
|
|
631
|
+
let eventsToReplay;
|
|
632
|
+
if (lastEventId) {
|
|
633
|
+
// Reconnection — replay all events since the last received (uncapped)
|
|
634
|
+
const idx = existingEvents.findIndex((e) => e.id === lastEventId);
|
|
635
|
+
eventsToReplay = idx >= 0 ? existingEvents.slice(idx + 1) : existingEvents;
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
// Fresh connection — only replay the most recent events
|
|
639
|
+
eventsToReplay =
|
|
640
|
+
existingEvents.length > MAX_REPLAY_EVENTS ? existingEvents.slice(-MAX_REPLAY_EVENTS) : existingEvents;
|
|
641
|
+
}
|
|
642
|
+
for (const event of eventsToReplay) {
|
|
643
|
+
writeSSE(res, event);
|
|
644
|
+
}
|
|
645
|
+
// If run already finished, close stream
|
|
646
|
+
if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
|
|
647
|
+
res.write('event: stream-end\ndata: {"reason":"run_finished"}\n\n');
|
|
648
|
+
res.end();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
// Stream live events
|
|
652
|
+
const onEvent = (event) => {
|
|
653
|
+
if (event.runId !== runId)
|
|
654
|
+
return;
|
|
655
|
+
writeSSE(res, event);
|
|
656
|
+
// Auto-close when run finishes
|
|
657
|
+
if (event.type === "RUN_COMPLETED" || event.type === "RUN_FAILED") {
|
|
658
|
+
res.write('event: stream-end\ndata: {"reason":"run_finished"}\n\n');
|
|
659
|
+
res.end();
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
t.on("event", onEvent);
|
|
663
|
+
// Heartbeat to keep connection alive
|
|
664
|
+
const heartbeat = setInterval(() => {
|
|
665
|
+
res.write(":heartbeat\n\n");
|
|
666
|
+
}, 5000);
|
|
667
|
+
// Cleanup on disconnect
|
|
668
|
+
req.on("close", () => {
|
|
669
|
+
t.removeListener("event", onEvent);
|
|
670
|
+
clearInterval(heartbeat);
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
/**
|
|
674
|
+
* Global SSE stream for all run events (dashboard live feed).
|
|
675
|
+
* Optionally filtered by workflow names.
|
|
676
|
+
*/
|
|
677
|
+
router.get("/stream", (req, res) => {
|
|
678
|
+
const workflowFilter = req.query.workflows ? req.query.workflows.split(",").map((w) => w.trim()) : null;
|
|
679
|
+
// Set SSE headers
|
|
680
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
681
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
682
|
+
res.setHeader("Connection", "keep-alive");
|
|
683
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
684
|
+
res.flushHeaders();
|
|
685
|
+
// Immediate acknowledgment so the browser fires onopen without waiting
|
|
686
|
+
res.write(`event: connected\ndata: ${JSON.stringify({ timestamp: Date.now() })}\n\n`);
|
|
687
|
+
res.write("retry: 3000\n\n");
|
|
688
|
+
const onEvent = (event) => {
|
|
689
|
+
if (workflowFilter && !workflowFilter.includes(event.workflowName))
|
|
690
|
+
return;
|
|
691
|
+
writeSSE(res, event);
|
|
692
|
+
};
|
|
693
|
+
t.on("event", onEvent);
|
|
694
|
+
// Heartbeat
|
|
695
|
+
const heartbeat = setInterval(() => {
|
|
696
|
+
res.write(":heartbeat\n\n");
|
|
697
|
+
}, 5000);
|
|
698
|
+
req.on("close", () => {
|
|
699
|
+
t.removeListener("event", onEvent);
|
|
700
|
+
clearInterval(heartbeat);
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
function writeSSE(res, event) {
|
|
705
|
+
res.write(`event: ${event.type}\n`);
|
|
706
|
+
res.write(`id: ${event.id}\n`);
|
|
707
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
708
|
+
}
|
|
709
|
+
// === CSV Helpers ===
|
|
710
|
+
function escapeCsv(value) {
|
|
711
|
+
if (value === null || value === undefined)
|
|
712
|
+
return "";
|
|
713
|
+
const str = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
714
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
|
715
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
716
|
+
}
|
|
717
|
+
return str;
|
|
718
|
+
}
|
|
719
|
+
function runsToCsv(runs) {
|
|
720
|
+
const headers = [
|
|
721
|
+
"id",
|
|
722
|
+
"workflowName",
|
|
723
|
+
"workflowPath",
|
|
724
|
+
"triggerType",
|
|
725
|
+
"triggerSummary",
|
|
726
|
+
"status",
|
|
727
|
+
"startedAt",
|
|
728
|
+
"finishedAt",
|
|
729
|
+
"durationMs",
|
|
730
|
+
"nodeCount",
|
|
731
|
+
"completedNodes",
|
|
732
|
+
"error",
|
|
733
|
+
"tags",
|
|
734
|
+
];
|
|
735
|
+
const rows = runs.map((r) => [
|
|
736
|
+
r.id,
|
|
737
|
+
r.workflowName,
|
|
738
|
+
r.workflowPath,
|
|
739
|
+
r.triggerType,
|
|
740
|
+
r.triggerSummary,
|
|
741
|
+
r.status,
|
|
742
|
+
new Date(r.startedAt).toISOString(),
|
|
743
|
+
r.finishedAt ? new Date(r.finishedAt).toISOString() : "",
|
|
744
|
+
r.durationMs ?? "",
|
|
745
|
+
r.nodeCount,
|
|
746
|
+
r.completedNodes,
|
|
747
|
+
r.error?.message ?? "",
|
|
748
|
+
(r.tags || []).join(";"),
|
|
749
|
+
]);
|
|
750
|
+
return `${[headers.join(","), ...rows.map((row) => row.map(escapeCsv).join(","))].join("\n")}\n`;
|
|
751
|
+
}
|
|
752
|
+
function singleRunToCsv(run, nodes, logs) {
|
|
753
|
+
let csv = "# Run Summary\n";
|
|
754
|
+
csv +=
|
|
755
|
+
"id,workflowName,triggerType,triggerSummary,status,startedAt,finishedAt,durationMs,nodeCount,completedNodes,error\n";
|
|
756
|
+
csv += `${[
|
|
757
|
+
run.id,
|
|
758
|
+
run.workflowName,
|
|
759
|
+
run.triggerType,
|
|
760
|
+
run.triggerSummary,
|
|
761
|
+
run.status,
|
|
762
|
+
new Date(run.startedAt).toISOString(),
|
|
763
|
+
run.finishedAt ? new Date(run.finishedAt).toISOString() : "",
|
|
764
|
+
run.durationMs ?? "",
|
|
765
|
+
run.nodeCount,
|
|
766
|
+
run.completedNodes,
|
|
767
|
+
run.error?.message ?? "",
|
|
768
|
+
]
|
|
769
|
+
.map(escapeCsv)
|
|
770
|
+
.join(",")}\n`;
|
|
771
|
+
csv += "\n# Nodes\n";
|
|
772
|
+
csv += "id,nodeName,nodeType,runtimeKind,status,startedAt,finishedAt,durationMs,stepIndex,depth,error\n";
|
|
773
|
+
for (const n of nodes) {
|
|
774
|
+
csv += `${[
|
|
775
|
+
n.id,
|
|
776
|
+
n.nodeName,
|
|
777
|
+
n.nodeType,
|
|
778
|
+
n.runtimeKind ?? "",
|
|
779
|
+
n.status,
|
|
780
|
+
new Date(n.startedAt).toISOString(),
|
|
781
|
+
n.finishedAt ? new Date(n.finishedAt).toISOString() : "",
|
|
782
|
+
n.durationMs ?? "",
|
|
783
|
+
n.stepIndex,
|
|
784
|
+
n.depth,
|
|
785
|
+
n.error?.message ?? "",
|
|
786
|
+
]
|
|
787
|
+
.map(escapeCsv)
|
|
788
|
+
.join(",")}\n`;
|
|
789
|
+
}
|
|
790
|
+
csv += "\n# Logs\n";
|
|
791
|
+
csv += "id,nodeName,level,message,timestamp\n";
|
|
792
|
+
for (const l of logs) {
|
|
793
|
+
csv += `${[l.id, l.nodeName ?? "", l.level, l.message, new Date(l.timestamp).toISOString()]
|
|
794
|
+
.map(escapeCsv)
|
|
795
|
+
.join(",")}\n`;
|
|
796
|
+
}
|
|
797
|
+
return csv;
|
|
798
|
+
}
|
|
799
|
+
// === AI Error Explanation Helpers ===
|
|
800
|
+
function buildNodeErrorContext(run, node, allNodes, nodeLogs) {
|
|
801
|
+
const timeline = allNodes
|
|
802
|
+
.sort((a, b) => a.stepIndex - b.stepIndex)
|
|
803
|
+
.map((n) => ` [${n.stepIndex}] ${n.nodeName} (${n.nodeType}${n.runtimeKind ? `, ${n.runtimeKind}` : ""}) → ${n.status}${n.durationMs ? ` (${n.durationMs}ms)` : ""}`)
|
|
804
|
+
.join("\n");
|
|
805
|
+
const logLines = nodeLogs
|
|
806
|
+
.slice(-20)
|
|
807
|
+
.map((l) => ` [${l.level.toUpperCase()}] ${l.message}`)
|
|
808
|
+
.join("\n");
|
|
809
|
+
return `You are a workflow debugging assistant. A node failed during a Blok workflow execution. Analyze the error and provide:
|
|
810
|
+
1. A clear explanation of what went wrong
|
|
811
|
+
2. The likely root cause
|
|
812
|
+
3. Suggested fixes
|
|
813
|
+
|
|
814
|
+
## Workflow Context
|
|
815
|
+
- Workflow: ${run.workflowName} (${run.workflowPath})
|
|
816
|
+
- Trigger: ${run.triggerSummary}
|
|
817
|
+
- Status: ${run.status}
|
|
818
|
+
|
|
819
|
+
## Node Execution Timeline
|
|
820
|
+
${timeline}
|
|
821
|
+
|
|
822
|
+
## Failed Node Details
|
|
823
|
+
- Name: ${node.nodeName}
|
|
824
|
+
- Type: ${node.nodeType}${node.runtimeKind ? `\n- Runtime: ${node.runtimeKind}` : ""}
|
|
825
|
+
- Step Index: ${node.stepIndex}
|
|
826
|
+
- Duration: ${node.durationMs ?? "N/A"}ms
|
|
827
|
+
|
|
828
|
+
## Error
|
|
829
|
+
- Message: ${node.error?.message ?? "Unknown"}${node.error?.code ? `\n- Code: ${node.error.code}` : ""}${node.error?.stack ? `\n- Stack Trace:\n${node.error.stack}` : ""}
|
|
830
|
+
|
|
831
|
+
## Node Input
|
|
832
|
+
${node.inputs ? JSON.stringify(node.inputs, null, 2).slice(0, 2000) : "N/A"}
|
|
833
|
+
|
|
834
|
+
## Node Output (before failure)
|
|
835
|
+
${node.outputs ? JSON.stringify(node.outputs, null, 2).slice(0, 2000) : "N/A"}
|
|
836
|
+
|
|
837
|
+
${logLines ? `## Node Logs (last 20)\n${logLines}` : ""}
|
|
838
|
+
|
|
839
|
+
Provide a concise, actionable explanation. Focus on the root cause and how to fix it.`;
|
|
840
|
+
}
|
|
841
|
+
function buildRunErrorContext(run, allNodes, failedNodes, logs) {
|
|
842
|
+
const timeline = allNodes
|
|
843
|
+
.sort((a, b) => a.stepIndex - b.stepIndex)
|
|
844
|
+
.map((n) => ` [${n.stepIndex}] ${n.nodeName} (${n.nodeType}${n.runtimeKind ? `, ${n.runtimeKind}` : ""}) → ${n.status}${n.durationMs ? ` (${n.durationMs}ms)` : ""}`)
|
|
845
|
+
.join("\n");
|
|
846
|
+
const failedDetails = failedNodes
|
|
847
|
+
.map((n) => `### ${n.nodeName}\n- Error: ${n.error?.message || "Unknown"}\n${n.error?.stack ? `- Stack: ${n.error.stack.split("\n").slice(0, 5).join("\n")}` : ""}${n.inputs ? `\n- Input: ${JSON.stringify(n.inputs, null, 2).slice(0, 500)}` : ""}`)
|
|
848
|
+
.join("\n\n");
|
|
849
|
+
const errorLogs = logs
|
|
850
|
+
.filter((l) => l.level === "error" || l.level === "warn")
|
|
851
|
+
.slice(-15)
|
|
852
|
+
.map((l) => ` [${l.level.toUpperCase()}]${l.nodeName ? ` (${l.nodeName})` : ""} ${l.message}`)
|
|
853
|
+
.join("\n");
|
|
854
|
+
return `You are a workflow debugging assistant. A Blok workflow execution failed. Analyze the error and provide:
|
|
855
|
+
1. A clear explanation of what went wrong
|
|
856
|
+
2. The likely root cause
|
|
857
|
+
3. Suggested fixes
|
|
858
|
+
|
|
859
|
+
## Workflow Context
|
|
860
|
+
- Workflow: ${run.workflowName} (${run.workflowPath})
|
|
861
|
+
- Trigger: ${run.triggerSummary}
|
|
862
|
+
- Duration: ${run.durationMs ?? "N/A"}ms
|
|
863
|
+
- Nodes: ${run.completedNodes}/${run.nodeCount} completed
|
|
864
|
+
|
|
865
|
+
## Run Error
|
|
866
|
+
- Message: ${run.error?.message ?? "Unknown"}${run.error?.code ? `\n- Code: ${run.error.code}` : ""}${run.error?.stack ? `\n- Stack Trace:\n${run.error.stack}` : ""}
|
|
867
|
+
|
|
868
|
+
## Node Execution Timeline
|
|
869
|
+
${timeline}
|
|
870
|
+
|
|
871
|
+
${failedDetails ? `## Failed Nodes\n${failedDetails}` : ""}
|
|
872
|
+
|
|
873
|
+
${errorLogs ? `## Error/Warning Logs\n${errorLogs}` : ""}
|
|
874
|
+
|
|
875
|
+
Provide a concise, actionable explanation. Focus on the root cause and how to fix it.`;
|
|
876
|
+
}
|
|
877
|
+
async function callOpenAI(apiKey, model, prompt) {
|
|
878
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
879
|
+
method: "POST",
|
|
880
|
+
headers: {
|
|
881
|
+
"Content-Type": "application/json",
|
|
882
|
+
Authorization: `Bearer ${apiKey}`,
|
|
883
|
+
},
|
|
884
|
+
body: JSON.stringify({
|
|
885
|
+
model,
|
|
886
|
+
messages: [
|
|
887
|
+
{
|
|
888
|
+
role: "system",
|
|
889
|
+
content: "You are an expert workflow debugging assistant for Blok, a workflow orchestration framework. Provide concise, actionable debugging advice. Use markdown formatting for readability.",
|
|
890
|
+
},
|
|
891
|
+
{ role: "user", content: prompt },
|
|
892
|
+
],
|
|
893
|
+
temperature: 0.3,
|
|
894
|
+
max_tokens: 1500,
|
|
895
|
+
}),
|
|
896
|
+
});
|
|
897
|
+
if (!response.ok) {
|
|
898
|
+
const err = await response.json().catch(() => ({}));
|
|
899
|
+
throw new Error(err.error?.message || `OpenAI API returned ${response.status}`);
|
|
900
|
+
}
|
|
901
|
+
const data = (await response.json());
|
|
902
|
+
return data.choices[0]?.message?.content || "No explanation generated.";
|
|
903
|
+
}
|
|
904
|
+
//# sourceMappingURL=TraceRouter.js.map
|