lopata 0.0.1

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.
Files changed (147) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/runtime/bindings/ai.ts +132 -0
  4. package/runtime/bindings/analytics-engine.ts +96 -0
  5. package/runtime/bindings/browser.ts +64 -0
  6. package/runtime/bindings/cache.ts +179 -0
  7. package/runtime/bindings/cf-streams.ts +56 -0
  8. package/runtime/bindings/container-docker.ts +225 -0
  9. package/runtime/bindings/container.ts +662 -0
  10. package/runtime/bindings/crypto-extras.ts +89 -0
  11. package/runtime/bindings/d1.ts +315 -0
  12. package/runtime/bindings/do-executor-inprocess.ts +140 -0
  13. package/runtime/bindings/do-executor-worker.ts +368 -0
  14. package/runtime/bindings/do-executor.ts +45 -0
  15. package/runtime/bindings/do-websocket-bridge.ts +70 -0
  16. package/runtime/bindings/do-worker-entry.ts +220 -0
  17. package/runtime/bindings/do-worker-env.ts +74 -0
  18. package/runtime/bindings/durable-object.ts +992 -0
  19. package/runtime/bindings/email.ts +180 -0
  20. package/runtime/bindings/html-rewriter.ts +84 -0
  21. package/runtime/bindings/hyperdrive.ts +130 -0
  22. package/runtime/bindings/images.ts +381 -0
  23. package/runtime/bindings/kv.ts +359 -0
  24. package/runtime/bindings/queue.ts +507 -0
  25. package/runtime/bindings/r2.ts +759 -0
  26. package/runtime/bindings/rpc-stub.ts +267 -0
  27. package/runtime/bindings/scheduled.ts +172 -0
  28. package/runtime/bindings/service-binding.ts +217 -0
  29. package/runtime/bindings/static-assets.ts +481 -0
  30. package/runtime/bindings/websocket-pair.ts +182 -0
  31. package/runtime/bindings/workflow.ts +858 -0
  32. package/runtime/bunflare-config.ts +56 -0
  33. package/runtime/cli/cache.ts +39 -0
  34. package/runtime/cli/context.ts +105 -0
  35. package/runtime/cli/d1.ts +163 -0
  36. package/runtime/cli/dev.ts +392 -0
  37. package/runtime/cli/kv.ts +84 -0
  38. package/runtime/cli/queues.ts +109 -0
  39. package/runtime/cli/r2.ts +140 -0
  40. package/runtime/cli/traces.ts +251 -0
  41. package/runtime/cli.ts +102 -0
  42. package/runtime/config.ts +148 -0
  43. package/runtime/d1-migrate.ts +37 -0
  44. package/runtime/dashboard/api.ts +174 -0
  45. package/runtime/dashboard/app.tsx +220 -0
  46. package/runtime/dashboard/components/breadcrumb.tsx +16 -0
  47. package/runtime/dashboard/components/buttons.tsx +13 -0
  48. package/runtime/dashboard/components/code-block.tsx +5 -0
  49. package/runtime/dashboard/components/detail-field.tsx +8 -0
  50. package/runtime/dashboard/components/empty-state.tsx +8 -0
  51. package/runtime/dashboard/components/filter-input.tsx +11 -0
  52. package/runtime/dashboard/components/index.ts +16 -0
  53. package/runtime/dashboard/components/key-value-table.tsx +23 -0
  54. package/runtime/dashboard/components/modal.tsx +23 -0
  55. package/runtime/dashboard/components/page-header.tsx +11 -0
  56. package/runtime/dashboard/components/pill-button.tsx +14 -0
  57. package/runtime/dashboard/components/refresh-button.tsx +7 -0
  58. package/runtime/dashboard/components/service-info.tsx +45 -0
  59. package/runtime/dashboard/components/status-badge.tsx +7 -0
  60. package/runtime/dashboard/components/table-link.tsx +5 -0
  61. package/runtime/dashboard/components/table.tsx +26 -0
  62. package/runtime/dashboard/components.tsx +19 -0
  63. package/runtime/dashboard/index.html +23 -0
  64. package/runtime/dashboard/lib.ts +45 -0
  65. package/runtime/dashboard/rpc/client.ts +20 -0
  66. package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
  67. package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
  68. package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
  69. package/runtime/dashboard/rpc/handlers/config.ts +137 -0
  70. package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
  71. package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
  72. package/runtime/dashboard/rpc/handlers/do.ts +117 -0
  73. package/runtime/dashboard/rpc/handlers/email.ts +82 -0
  74. package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
  75. package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
  76. package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
  77. package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
  78. package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
  79. package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
  80. package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
  81. package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
  82. package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
  83. package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
  84. package/runtime/dashboard/rpc/hooks.ts +132 -0
  85. package/runtime/dashboard/rpc/server.ts +70 -0
  86. package/runtime/dashboard/rpc/types.ts +396 -0
  87. package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
  88. package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
  89. package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
  90. package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
  91. package/runtime/dashboard/sql-browser/hooks.ts +137 -0
  92. package/runtime/dashboard/sql-browser/index.ts +4 -0
  93. package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
  94. package/runtime/dashboard/sql-browser/modals.tsx +116 -0
  95. package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
  96. package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
  97. package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
  98. package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
  99. package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
  100. package/runtime/dashboard/sql-browser/types.ts +61 -0
  101. package/runtime/dashboard/sql-browser/utils.ts +167 -0
  102. package/runtime/dashboard/style.css +177 -0
  103. package/runtime/dashboard/views/ai.tsx +152 -0
  104. package/runtime/dashboard/views/analytics-engine.tsx +169 -0
  105. package/runtime/dashboard/views/cache.tsx +93 -0
  106. package/runtime/dashboard/views/containers.tsx +197 -0
  107. package/runtime/dashboard/views/d1.tsx +81 -0
  108. package/runtime/dashboard/views/do.tsx +168 -0
  109. package/runtime/dashboard/views/email.tsx +235 -0
  110. package/runtime/dashboard/views/errors.tsx +558 -0
  111. package/runtime/dashboard/views/home.tsx +287 -0
  112. package/runtime/dashboard/views/kv.tsx +273 -0
  113. package/runtime/dashboard/views/queue.tsx +193 -0
  114. package/runtime/dashboard/views/r2.tsx +202 -0
  115. package/runtime/dashboard/views/scheduled.tsx +89 -0
  116. package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
  117. package/runtime/dashboard/views/traces.tsx +768 -0
  118. package/runtime/dashboard/views/workers.tsx +55 -0
  119. package/runtime/dashboard/views/workflows.tsx +473 -0
  120. package/runtime/db.ts +258 -0
  121. package/runtime/env.ts +362 -0
  122. package/runtime/error-page/app.tsx +394 -0
  123. package/runtime/error-page/build.ts +269 -0
  124. package/runtime/error-page/index.html +16 -0
  125. package/runtime/error-page/style.css +31 -0
  126. package/runtime/execution-context.ts +18 -0
  127. package/runtime/file-watcher.ts +57 -0
  128. package/runtime/generation-manager.ts +230 -0
  129. package/runtime/generation.ts +411 -0
  130. package/runtime/plugin.ts +292 -0
  131. package/runtime/request-cf.ts +28 -0
  132. package/runtime/rpc-validate.ts +154 -0
  133. package/runtime/tracing/context.ts +40 -0
  134. package/runtime/tracing/db.ts +73 -0
  135. package/runtime/tracing/frames.ts +75 -0
  136. package/runtime/tracing/instrument.ts +186 -0
  137. package/runtime/tracing/span.ts +138 -0
  138. package/runtime/tracing/store.ts +499 -0
  139. package/runtime/tracing/types.ts +47 -0
  140. package/runtime/vite-plugin/config-plugin.ts +68 -0
  141. package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
  142. package/runtime/vite-plugin/dist/index.mjs +52333 -0
  143. package/runtime/vite-plugin/globals-plugin.ts +94 -0
  144. package/runtime/vite-plugin/index.ts +43 -0
  145. package/runtime/vite-plugin/modules-plugin.ts +88 -0
  146. package/runtime/vite-plugin/react-router-plugin.ts +95 -0
  147. package/runtime/worker-registry.ts +52 -0
@@ -0,0 +1,55 @@
1
+ import { useQuery } from "../rpc/hooks";
2
+ import { EmptyState, PageHeader, Table, TableLink, StatusBadge } from "../components";
3
+
4
+ const TYPE_COLORS: Record<string, string> = {
5
+ kv: "bg-emerald-100 text-emerald-700",
6
+ r2: "bg-blue-100 text-blue-700",
7
+ d1: "bg-violet-100 text-violet-700",
8
+ do: "bg-amber-100 text-amber-700",
9
+ queue: "bg-rose-100 text-rose-700",
10
+ workflow: "bg-cyan-100 text-cyan-700",
11
+ service: "bg-panel-active text-text-data",
12
+ images: "bg-pink-100 text-pink-700",
13
+ };
14
+
15
+ export function WorkersView() {
16
+ const { data: workers } = useQuery("workers.list");
17
+
18
+ return (
19
+ <div class="p-8">
20
+ <PageHeader title="Workers" subtitle={`${workers?.length ?? 0} worker(s)`} />
21
+ {!workers?.length ? (
22
+ <EmptyState message="No workers configured" />
23
+ ) : (
24
+ <div class="space-y-8">
25
+ {workers.map(w => (
26
+ <div key={w.name}>
27
+ <div class="flex items-center gap-3 mb-4">
28
+ <span class="w-7 h-7 rounded-md bg-panel-hover flex items-center justify-center text-sm">⊡</span>
29
+ <h2 class="text-lg font-bold text-ink">{w.name}</h2>
30
+ {w.isMain && (
31
+ <span class="px-2 py-0.5 rounded-md text-xs font-medium bg-gray-900 text-white">main</span>
32
+ )}
33
+ <span class="text-xs text-text-muted">{w.bindings.length} binding(s)</span>
34
+ </div>
35
+ {w.bindings.length === 0 ? (
36
+ <EmptyState message="No bindings configured" />
37
+ ) : (
38
+ <Table
39
+ headers={["Type", "Binding", "Target"]}
40
+ rows={w.bindings.map(b => [
41
+ <StatusBadge status={b.type} colorMap={TYPE_COLORS} />,
42
+ <span class="font-mono text-xs font-medium">{b.name}</span>,
43
+ b.href
44
+ ? <TableLink href={b.href}>{b.target}</TableLink>
45
+ : <span class="text-text-secondary">{b.target || "—"}</span>,
46
+ ])}
47
+ />
48
+ )}
49
+ </div>
50
+ ))}
51
+ </div>
52
+ )}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,473 @@
1
+ import { useState } from "preact/hooks";
2
+ import { formatTime } from "../lib";
3
+ import { useQuery, useMutation } from "../rpc/hooks";
4
+ import { EmptyState, Breadcrumb, Table, PageHeader, CodeBlock, TableLink, StatusBadge, ServiceInfo, RefreshButton } from "../components";
5
+
6
+ const WORKFLOW_STATUS_COLORS: Record<string, string> = {
7
+ running: "bg-accent-blue text-ink",
8
+ complete: "bg-emerald-100 text-emerald-700",
9
+ errored: "bg-red-100 text-red-700",
10
+ terminated: "bg-panel-active text-text-data",
11
+ waiting: "bg-blue-100 text-blue-700",
12
+ paused: "bg-amber-100 text-amber-700",
13
+ queued: "bg-purple-100 text-purple-700",
14
+ };
15
+
16
+ export function WorkflowsView({ route }: { route: string }) {
17
+ const parts = route.split("/").filter(Boolean);
18
+ if (parts.length === 1) return <WorkflowList />;
19
+ if (parts.length === 2) return <WorkflowInstanceList name={decodeURIComponent(parts[1]!)} />;
20
+ if (parts.length >= 3) return <WorkflowInstanceDetail name={decodeURIComponent(parts[1]!)} id={decodeURIComponent(parts[2]!)} />;
21
+ return null;
22
+ }
23
+
24
+ function WorkflowList() {
25
+ const { data: workflows, refetch } = useQuery("workflows.list");
26
+ const { data: configGroups } = useQuery("config.forService", { type: "workflows" });
27
+
28
+ const totalInstances = workflows?.reduce((s, w) => s + w.total, 0) ?? 0;
29
+ const totalRunning = workflows?.reduce((s, w) => s + (w.byStatus.running ?? 0), 0) ?? 0;
30
+ const totalErrored = workflows?.reduce((s, w) => s + (w.byStatus.errored ?? 0), 0) ?? 0;
31
+
32
+ return (
33
+ <div class="p-8 max-w-6xl">
34
+ <PageHeader title="Workflows" subtitle={`${workflows?.length ?? 0} workflow(s)`} actions={<RefreshButton onClick={refetch} />} />
35
+ <div class="flex gap-6 items-start">
36
+ <div class="flex-1 min-w-0">
37
+ {!workflows?.length ? (
38
+ <EmptyState message="No workflow instances found" />
39
+ ) : (
40
+ <Table
41
+ headers={["Workflow", "Total", "Running", "Complete", "Errored"]}
42
+ rows={workflows.map(w => [
43
+ <TableLink href={`#/workflows/${encodeURIComponent(w.name)}`}>{w.name}</TableLink>,
44
+ <span class="tabular-nums">{w.total}</span>,
45
+ w.byStatus.running ?? 0,
46
+ w.byStatus.complete ?? 0,
47
+ w.byStatus.errored ?? 0,
48
+ ])}
49
+ />
50
+ )}
51
+ </div>
52
+ <ServiceInfo
53
+ description="Durable execution engine for multi-step tasks."
54
+ stats={[
55
+ { label: "Instances", value: totalInstances.toLocaleString() },
56
+ { label: "Running", value: totalRunning.toLocaleString() },
57
+ { label: "Errored", value: totalErrored.toLocaleString() },
58
+ ]}
59
+ configGroups={configGroups}
60
+ links={[
61
+ { label: "Documentation", href: "https://developers.cloudflare.com/workflows/" },
62
+ { label: "API Reference", href: "https://developers.cloudflare.com/api/resources/workflows/" },
63
+ ]}
64
+ />
65
+ </div>
66
+ </div>
67
+ );
68
+ }
69
+
70
+ function CreateWorkflowForm({ name, onCreated }: { name: string; onCreated: (id: string) => void }) {
71
+ const [open, setOpen] = useState(false);
72
+ const [params, setParams] = useState("{}");
73
+ const [error, setError] = useState("");
74
+ const create = useMutation("workflows.create");
75
+
76
+ const handleSubmit = async () => {
77
+ setError("");
78
+ const result = await create.mutate({ name, params });
79
+ if (result) {
80
+ setParams("{}");
81
+ setOpen(false);
82
+ onCreated(result.id);
83
+ } else if (create.error) {
84
+ setError(create.error.message);
85
+ }
86
+ };
87
+
88
+ if (!open) {
89
+ return (
90
+ <button
91
+ onClick={() => setOpen(true)}
92
+ class="rounded-md px-3 py-1.5 text-sm font-medium bg-ink text-surface hover:opacity-80 transition-all"
93
+ >
94
+ Create instance
95
+ </button>
96
+ );
97
+ }
98
+
99
+ return (
100
+ <div class="bg-panel border border-border rounded-lg p-4 mb-6">
101
+ <div class="flex items-center justify-between mb-3">
102
+ <div class="text-sm font-semibold text-ink">Create workflow instance</div>
103
+ <button onClick={() => { setOpen(false); setError(""); }} class="text-text-muted hover:text-text-data text-xs font-medium">
104
+ Cancel
105
+ </button>
106
+ </div>
107
+ <textarea
108
+ value={params}
109
+ onInput={e => setParams((e.target as HTMLTextAreaElement).value)}
110
+ placeholder='{"key": "value"}'
111
+ class="w-full bg-panel-secondary border border-border rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-border focus:ring-1 focus:ring-border transition-all resize-y min-h-[80px]"
112
+ rows={3}
113
+ />
114
+ {error && <div class="text-red-500 text-xs mt-1">{error}</div>}
115
+ <div class="flex justify-end mt-3">
116
+ <button
117
+ onClick={handleSubmit}
118
+ disabled={create.isLoading || !params.trim()}
119
+ class="rounded-md px-4 py-1.5 text-sm font-medium bg-ink text-surface hover:opacity-80 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
120
+ >
121
+ {create.isLoading ? "Creating..." : "Create"}
122
+ </button>
123
+ </div>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ function ActionButton({ onClick, label, color = "blue", disabled }: { onClick: () => void; label: string; color?: "blue" | "red" | "amber" | "emerald"; disabled?: boolean }) {
129
+ const colors = {
130
+ blue: "text-blue-500 hover:text-blue-700 hover:bg-blue-50",
131
+ red: "text-red-400 hover:text-red-600 hover:bg-red-50",
132
+ amber: "text-amber-500 hover:text-amber-700 hover:bg-amber-50",
133
+ emerald: "text-emerald-500 hover:text-emerald-700 hover:bg-emerald-50",
134
+ };
135
+ return (
136
+ <button
137
+ onClick={onClick}
138
+ disabled={disabled}
139
+ class={`text-xs font-medium rounded-md px-2 py-1 transition-all disabled:opacity-50 disabled:cursor-not-allowed ${colors[color]}`}
140
+ >
141
+ {label}
142
+ </button>
143
+ );
144
+ }
145
+
146
+ function InstanceActions({ name, id, status, refetch, onDuplicated }: { name: string; id: string; status: string; refetch: () => void; onDuplicated?: (id: string) => void }) {
147
+ const pause = useMutation("workflows.pause");
148
+ const resume = useMutation("workflows.resume");
149
+ const terminate = useMutation("workflows.terminate");
150
+ const restart = useMutation("workflows.restart");
151
+ const duplicate = useMutation("workflows.duplicate");
152
+
153
+ const handlePause = async () => { await pause.mutate({ name, id }); refetch(); };
154
+ const handleResume = async () => { await resume.mutate({ name, id }); refetch(); };
155
+ const handleTerminate = async () => {
156
+ if (!confirm("Terminate this workflow instance?")) return;
157
+ await terminate.mutate({ name, id }); refetch();
158
+ };
159
+ const handleRestart = async () => {
160
+ if (!confirm("Restart this workflow instance? All steps will re-execute.")) return;
161
+ await restart.mutate({ name, id }); refetch();
162
+ };
163
+ const handleDuplicate = async () => {
164
+ const result = await duplicate.mutate({ name, id });
165
+ if (result && onDuplicated) onDuplicated(result.id);
166
+ else refetch();
167
+ };
168
+
169
+ const isTerminal = ["complete", "errored", "terminated"].includes(status);
170
+
171
+ return (
172
+ <div class="flex gap-1">
173
+ {(status === "running" || status === "waiting") && (
174
+ <>
175
+ <ActionButton onClick={handlePause} label="Pause" color="amber" />
176
+ <ActionButton onClick={handleTerminate} label="Terminate" color="red" />
177
+ </>
178
+ )}
179
+ {status === "paused" && (
180
+ <>
181
+ <ActionButton onClick={handleResume} label="Resume" color="emerald" />
182
+ <ActionButton onClick={handleTerminate} label="Terminate" color="red" />
183
+ </>
184
+ )}
185
+ {status === "queued" && (
186
+ <ActionButton onClick={handleTerminate} label="Terminate" color="red" />
187
+ )}
188
+ {isTerminal && (
189
+ <>
190
+ <ActionButton onClick={handleRestart} label="Restart" color="blue" />
191
+ <ActionButton onClick={handleDuplicate} label="Duplicate" color="blue" />
192
+ </>
193
+ )}
194
+ </div>
195
+ );
196
+ }
197
+
198
+ function WorkflowInstanceList({ name }: { name: string }) {
199
+ const [statusFilter, setStatusFilter] = useState("");
200
+ const { data: instances, refetch } = useQuery("workflows.listInstances", { name, status: statusFilter || undefined });
201
+
202
+ const handleCreated = (_id: string) => { refetch(); };
203
+ const handleDuplicated = (id: string) => { location.hash = `#/workflows/${encodeURIComponent(name)}/${encodeURIComponent(id)}`; };
204
+
205
+ return (
206
+ <div class="p-8">
207
+ <Breadcrumb items={[{ label: "Workflows", href: "#/workflows" }, { label: name }]} />
208
+ <div class="mb-6 flex gap-2 items-center justify-between">
209
+ <select
210
+ value={statusFilter}
211
+ onChange={e => setStatusFilter((e.target as HTMLSelectElement).value)}
212
+ class="bg-panel border border-border rounded-lg px-3 py-2 text-sm outline-none focus:border-border focus:ring-1 focus:ring-border transition-all appearance-none pr-10"
213
+ >
214
+ <option value="">All statuses</option>
215
+ <option value="running">Running</option>
216
+ <option value="waiting">Waiting</option>
217
+ <option value="paused">Paused</option>
218
+ <option value="queued">Queued</option>
219
+ <option value="complete">Complete</option>
220
+ <option value="errored">Errored</option>
221
+ <option value="terminated">Terminated</option>
222
+ </select>
223
+ <div class="flex gap-2 items-center">
224
+ <RefreshButton onClick={refetch} />
225
+ <CreateWorkflowForm name={name} onCreated={handleCreated} />
226
+ </div>
227
+ </div>
228
+ {!instances?.length ? (
229
+ <EmptyState message="No instances found" />
230
+ ) : (
231
+ <Table
232
+ headers={["Instance ID", "Status", "Created", "Updated", ""]}
233
+ rows={instances.map(inst => [
234
+ <TableLink href={`#/workflows/${encodeURIComponent(name)}/${encodeURIComponent(inst.id)}`} mono>{inst.id.slice(0, 16)}...</TableLink>,
235
+ <StatusBadge status={inst.status} colorMap={WORKFLOW_STATUS_COLORS} />,
236
+ formatTime(inst.created_at),
237
+ formatTime(inst.updated_at),
238
+ <InstanceActions name={name} id={inst.id} status={inst.status} refetch={refetch} onDuplicated={handleDuplicated} />,
239
+ ])}
240
+ />
241
+ )}
242
+ </div>
243
+ );
244
+ }
245
+
246
+ function SkipSleepBanner({ name, id, activeSleep, refetch }: { name: string; id: string; activeSleep: { stepName: string; until: number }; refetch: () => void }) {
247
+ const skipSleep = useMutation("workflows.skipSleep");
248
+ const remaining = Math.max(0, activeSleep.until - Date.now());
249
+ const label = activeSleep.stepName.replace(/^(sleep|sleepUntil):/, "");
250
+
251
+ const formatRemaining = (ms: number) => {
252
+ if (ms < 1000) return "< 1s";
253
+ const s = Math.floor(ms / 1000);
254
+ if (s < 60) return `${s}s`;
255
+ const m = Math.floor(s / 60);
256
+ if (m < 60) return `${m}m ${s % 60}s`;
257
+ const h = Math.floor(m / 60);
258
+ return `${h}h ${m % 60}m`;
259
+ };
260
+
261
+ const handleSkip = async () => {
262
+ await skipSleep.mutate({ name, id });
263
+ refetch();
264
+ };
265
+
266
+ return (
267
+ <div class="mb-6 bg-amber-50 border border-amber-200 rounded-lg p-4 flex items-center justify-between">
268
+ <div>
269
+ <span class="text-sm font-semibold text-amber-800">Sleeping</span>
270
+ <span class="text-sm text-amber-700 ml-2">
271
+ step "{label}" — {formatRemaining(remaining)} remaining
272
+ </span>
273
+ </div>
274
+ <button
275
+ onClick={handleSkip}
276
+ disabled={skipSleep.isLoading}
277
+ class="rounded-md px-3 py-1.5 text-sm font-medium bg-amber-600 text-white hover:bg-amber-700 transition-all disabled:opacity-50"
278
+ >
279
+ {skipSleep.isLoading ? "Skipping..." : "Skip sleep"}
280
+ </button>
281
+ </div>
282
+ );
283
+ }
284
+
285
+ function SendEventForm({ name, id, waitingForEvents, refetch }: { name: string; id: string; waitingForEvents: string[]; refetch: () => void }) {
286
+ const [eventType, setEventType] = useState(waitingForEvents[0] ?? "");
287
+ const [payload, setPayload] = useState("{}");
288
+ const [error, setError] = useState("");
289
+ const sendEvent = useMutation("workflows.sendEvent");
290
+
291
+ const handleSend = async () => {
292
+ setError("");
293
+ try {
294
+ JSON.parse(payload); // validate JSON
295
+ } catch {
296
+ setError("Invalid JSON payload");
297
+ return;
298
+ }
299
+ const result = await sendEvent.mutate({ name, id, type: eventType, payload });
300
+ if (result) {
301
+ setPayload("{}");
302
+ refetch();
303
+ } else if (sendEvent.error) {
304
+ setError(sendEvent.error.message);
305
+ }
306
+ };
307
+
308
+ return (
309
+ <div class="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
310
+ <div class="text-sm font-semibold text-blue-800 mb-3">
311
+ Waiting for event{waitingForEvents.length > 0 && (
312
+ <span class="font-normal text-blue-600">
313
+ {" "}— type: {waitingForEvents.map(t => `"${t}"`).join(", ")}
314
+ </span>
315
+ )}
316
+ </div>
317
+ <div class="flex gap-3 items-start">
318
+ <div class="flex-1">
319
+ <input
320
+ type="text"
321
+ value={eventType}
322
+ onInput={e => setEventType((e.target as HTMLInputElement).value)}
323
+ placeholder="Event type"
324
+ class="w-full bg-white border border-blue-200 rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-300 transition-all mb-2"
325
+ />
326
+ <textarea
327
+ value={payload}
328
+ onInput={e => setPayload((e.target as HTMLTextAreaElement).value)}
329
+ placeholder='{"key": "value"}'
330
+ class="w-full bg-white border border-blue-200 rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-300 transition-all resize-y min-h-[60px]"
331
+ rows={2}
332
+ />
333
+ {error && <div class="text-red-500 text-xs mt-1">{error}</div>}
334
+ </div>
335
+ <button
336
+ onClick={handleSend}
337
+ disabled={sendEvent.isLoading || !eventType.trim()}
338
+ class="rounded-md px-4 py-2 text-sm font-medium bg-blue-600 text-white hover:bg-blue-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
339
+ >
340
+ {sendEvent.isLoading ? "Sending..." : "Send event"}
341
+ </button>
342
+ </div>
343
+ </div>
344
+ );
345
+ }
346
+
347
+ function WorkflowInstanceDetail({ name, id }: { name: string; id: string }) {
348
+ const { data, refetch } = useQuery("workflows.getInstance", { name, id });
349
+ const restartFromStep = useMutation("workflows.restart");
350
+
351
+ const handleDuplicated = (newId: string) => { location.hash = `#/workflows/${encodeURIComponent(name)}/${encodeURIComponent(newId)}`; };
352
+
353
+ const handleRestartFromStep = async (stepName: string) => {
354
+ if (!confirm(`Restart from step "${stepName}"? This step and all subsequent steps will re-execute.`)) return;
355
+ await restartFromStep.mutate({ name, id, fromStep: stepName });
356
+ refetch();
357
+ };
358
+
359
+ if (!data) return <div class="p-8 text-text-muted font-medium">Loading...</div>;
360
+
361
+ const isTerminal = ["complete", "errored", "terminated"].includes(data.status);
362
+
363
+ return (
364
+ <div class="p-8">
365
+ <Breadcrumb items={[
366
+ { label: "Workflows", href: "#/workflows" },
367
+ { label: name, href: `#/workflows/${encodeURIComponent(name)}` },
368
+ { label: id.slice(0, 16) + "..." },
369
+ ]} />
370
+
371
+ <div class="flex items-center gap-4 mb-8">
372
+ <StatusBadge status={data.status} colorMap={WORKFLOW_STATUS_COLORS} />
373
+ <span class="text-sm text-text-muted font-medium">Created: {formatTime(data.created_at)}</span>
374
+ <InstanceActions name={name} id={id} status={data.status} refetch={refetch} onDuplicated={handleDuplicated} />
375
+ <RefreshButton onClick={refetch} />
376
+ </div>
377
+
378
+ {data.activeSleep && (
379
+ <SkipSleepBanner name={name} id={id} activeSleep={data.activeSleep} refetch={refetch} />
380
+ )}
381
+
382
+ {data.status === "waiting" && (
383
+ <SendEventForm name={name} id={id} waitingForEvents={data.waitingForEvents} refetch={refetch} />
384
+ )}
385
+
386
+ {data.params && (
387
+ <div class="mb-6 bg-panel rounded-lg border border-border p-5">
388
+ <h3 class="text-xs font-semibold text-text-muted uppercase tracking-wider mb-3">Parameters</h3>
389
+ <CodeBlock>{data.params}</CodeBlock>
390
+ </div>
391
+ )}
392
+
393
+ {data.output && (
394
+ <div class="mb-6 bg-panel rounded-lg border border-border p-5">
395
+ <h3 class="text-xs font-semibold text-text-muted uppercase tracking-wider mb-3">Output</h3>
396
+ <CodeBlock>{data.output}</CodeBlock>
397
+ </div>
398
+ )}
399
+
400
+ {data.error && (
401
+ <div class="mb-6 bg-panel rounded-lg border border-border p-5">
402
+ <h3 class="text-xs font-semibold text-text-muted uppercase tracking-wider mb-3">Error</h3>
403
+ <pre class="bg-red-50 rounded-lg p-4 text-xs text-red-600 overflow-x-auto font-mono">{data.error}</pre>
404
+ </div>
405
+ )}
406
+
407
+ <div class="mb-6">
408
+ <h3 class="text-sm font-semibold text-ink mb-4">Steps ({data.steps.length})</h3>
409
+ {data.steps.length === 0 && data.stepAttempts.length === 0 ? (
410
+ <div class="text-text-muted text-sm font-medium">No steps completed yet</div>
411
+ ) : (
412
+ <Table
413
+ headers={["Step", "Output", "Completed", ...(isTerminal ? [""] : [])]}
414
+ rows={[
415
+ ...data.steps.map(s => {
416
+ const row = [
417
+ <span class="font-mono text-xs font-medium">{s.step_name}</span>,
418
+ s.output ? <pre class="text-xs max-w-md truncate font-mono">{s.output}</pre> : "\u2014",
419
+ formatTime(s.completed_at),
420
+ ];
421
+ if (isTerminal) {
422
+ row.push(
423
+ <ActionButton onClick={() => handleRestartFromStep(s.step_name)} label="Restart from here" color="blue" />
424
+ );
425
+ }
426
+ return row;
427
+ }),
428
+ ...data.stepAttempts.map(a => {
429
+ const errorContent = a.last_error ? (
430
+ a.last_error_id ? (
431
+ <a href={`#/errors/${encodeURIComponent(a.last_error_id)}`} class="text-xs max-w-md truncate font-mono text-red-600 dark:text-red-400 hover:underline block" title={a.last_error}>
432
+ {a.last_error_name ? `${a.last_error_name}: ` : ""}{a.last_error}
433
+ </a>
434
+ ) : (
435
+ <pre class="text-xs max-w-md truncate font-mono text-red-600 dark:text-red-400" title={a.last_error}>
436
+ {a.last_error_name ? `${a.last_error_name}: ` : ""}{a.last_error}
437
+ </pre>
438
+ )
439
+ ) : "\u2014";
440
+ const row = [
441
+ <span class="font-mono text-xs font-medium">
442
+ {a.step_name}
443
+ <span class="ml-2 text-amber-600 dark:text-amber-400 text-[10px] font-semibold uppercase">retrying ({a.failed_attempts}x failed)</span>
444
+ </span>,
445
+ errorContent,
446
+ a.updated_at ? formatTime(a.updated_at) : "\u2014",
447
+ ];
448
+ if (isTerminal) {
449
+ row.push("");
450
+ }
451
+ return row;
452
+ }),
453
+ ]}
454
+ />
455
+ )}
456
+ </div>
457
+
458
+ {data.events.length > 0 && (
459
+ <div>
460
+ <h3 class="text-sm font-semibold text-ink mb-4">Events ({data.events.length})</h3>
461
+ <Table
462
+ headers={["Type", "Payload", "Time"]}
463
+ rows={data.events.map(e => [
464
+ <span class="font-mono text-xs font-medium">{e.event_type}</span>,
465
+ e.payload ? <pre class="text-xs max-w-md truncate font-mono">{e.payload}</pre> : "\u2014",
466
+ formatTime(e.created_at),
467
+ ])}
468
+ />
469
+ </div>
470
+ )}
471
+ </div>
472
+ );
473
+ }