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.
- package/README.md +15 -0
- package/package.json +51 -0
- package/runtime/bindings/ai.ts +132 -0
- package/runtime/bindings/analytics-engine.ts +96 -0
- package/runtime/bindings/browser.ts +64 -0
- package/runtime/bindings/cache.ts +179 -0
- package/runtime/bindings/cf-streams.ts +56 -0
- package/runtime/bindings/container-docker.ts +225 -0
- package/runtime/bindings/container.ts +662 -0
- package/runtime/bindings/crypto-extras.ts +89 -0
- package/runtime/bindings/d1.ts +315 -0
- package/runtime/bindings/do-executor-inprocess.ts +140 -0
- package/runtime/bindings/do-executor-worker.ts +368 -0
- package/runtime/bindings/do-executor.ts +45 -0
- package/runtime/bindings/do-websocket-bridge.ts +70 -0
- package/runtime/bindings/do-worker-entry.ts +220 -0
- package/runtime/bindings/do-worker-env.ts +74 -0
- package/runtime/bindings/durable-object.ts +992 -0
- package/runtime/bindings/email.ts +180 -0
- package/runtime/bindings/html-rewriter.ts +84 -0
- package/runtime/bindings/hyperdrive.ts +130 -0
- package/runtime/bindings/images.ts +381 -0
- package/runtime/bindings/kv.ts +359 -0
- package/runtime/bindings/queue.ts +507 -0
- package/runtime/bindings/r2.ts +759 -0
- package/runtime/bindings/rpc-stub.ts +267 -0
- package/runtime/bindings/scheduled.ts +172 -0
- package/runtime/bindings/service-binding.ts +217 -0
- package/runtime/bindings/static-assets.ts +481 -0
- package/runtime/bindings/websocket-pair.ts +182 -0
- package/runtime/bindings/workflow.ts +858 -0
- package/runtime/bunflare-config.ts +56 -0
- package/runtime/cli/cache.ts +39 -0
- package/runtime/cli/context.ts +105 -0
- package/runtime/cli/d1.ts +163 -0
- package/runtime/cli/dev.ts +392 -0
- package/runtime/cli/kv.ts +84 -0
- package/runtime/cli/queues.ts +109 -0
- package/runtime/cli/r2.ts +140 -0
- package/runtime/cli/traces.ts +251 -0
- package/runtime/cli.ts +102 -0
- package/runtime/config.ts +148 -0
- package/runtime/d1-migrate.ts +37 -0
- package/runtime/dashboard/api.ts +174 -0
- package/runtime/dashboard/app.tsx +220 -0
- package/runtime/dashboard/components/breadcrumb.tsx +16 -0
- package/runtime/dashboard/components/buttons.tsx +13 -0
- package/runtime/dashboard/components/code-block.tsx +5 -0
- package/runtime/dashboard/components/detail-field.tsx +8 -0
- package/runtime/dashboard/components/empty-state.tsx +8 -0
- package/runtime/dashboard/components/filter-input.tsx +11 -0
- package/runtime/dashboard/components/index.ts +16 -0
- package/runtime/dashboard/components/key-value-table.tsx +23 -0
- package/runtime/dashboard/components/modal.tsx +23 -0
- package/runtime/dashboard/components/page-header.tsx +11 -0
- package/runtime/dashboard/components/pill-button.tsx +14 -0
- package/runtime/dashboard/components/refresh-button.tsx +7 -0
- package/runtime/dashboard/components/service-info.tsx +45 -0
- package/runtime/dashboard/components/status-badge.tsx +7 -0
- package/runtime/dashboard/components/table-link.tsx +5 -0
- package/runtime/dashboard/components/table.tsx +26 -0
- package/runtime/dashboard/components.tsx +19 -0
- package/runtime/dashboard/index.html +23 -0
- package/runtime/dashboard/lib.ts +45 -0
- package/runtime/dashboard/rpc/client.ts +20 -0
- package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
- package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
- package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
- package/runtime/dashboard/rpc/handlers/config.ts +137 -0
- package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
- package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
- package/runtime/dashboard/rpc/handlers/do.ts +117 -0
- package/runtime/dashboard/rpc/handlers/email.ts +82 -0
- package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
- package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
- package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
- package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
- package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
- package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
- package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
- package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
- package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
- package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
- package/runtime/dashboard/rpc/hooks.ts +132 -0
- package/runtime/dashboard/rpc/server.ts +70 -0
- package/runtime/dashboard/rpc/types.ts +396 -0
- package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
- package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
- package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
- package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
- package/runtime/dashboard/sql-browser/hooks.ts +137 -0
- package/runtime/dashboard/sql-browser/index.ts +4 -0
- package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
- package/runtime/dashboard/sql-browser/modals.tsx +116 -0
- package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
- package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
- package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
- package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
- package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
- package/runtime/dashboard/sql-browser/types.ts +61 -0
- package/runtime/dashboard/sql-browser/utils.ts +167 -0
- package/runtime/dashboard/style.css +177 -0
- package/runtime/dashboard/views/ai.tsx +152 -0
- package/runtime/dashboard/views/analytics-engine.tsx +169 -0
- package/runtime/dashboard/views/cache.tsx +93 -0
- package/runtime/dashboard/views/containers.tsx +197 -0
- package/runtime/dashboard/views/d1.tsx +81 -0
- package/runtime/dashboard/views/do.tsx +168 -0
- package/runtime/dashboard/views/email.tsx +235 -0
- package/runtime/dashboard/views/errors.tsx +558 -0
- package/runtime/dashboard/views/home.tsx +287 -0
- package/runtime/dashboard/views/kv.tsx +273 -0
- package/runtime/dashboard/views/queue.tsx +193 -0
- package/runtime/dashboard/views/r2.tsx +202 -0
- package/runtime/dashboard/views/scheduled.tsx +89 -0
- package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
- package/runtime/dashboard/views/traces.tsx +768 -0
- package/runtime/dashboard/views/workers.tsx +55 -0
- package/runtime/dashboard/views/workflows.tsx +473 -0
- package/runtime/db.ts +258 -0
- package/runtime/env.ts +362 -0
- package/runtime/error-page/app.tsx +394 -0
- package/runtime/error-page/build.ts +269 -0
- package/runtime/error-page/index.html +16 -0
- package/runtime/error-page/style.css +31 -0
- package/runtime/execution-context.ts +18 -0
- package/runtime/file-watcher.ts +57 -0
- package/runtime/generation-manager.ts +230 -0
- package/runtime/generation.ts +411 -0
- package/runtime/plugin.ts +292 -0
- package/runtime/request-cf.ts +28 -0
- package/runtime/rpc-validate.ts +154 -0
- package/runtime/tracing/context.ts +40 -0
- package/runtime/tracing/db.ts +73 -0
- package/runtime/tracing/frames.ts +75 -0
- package/runtime/tracing/instrument.ts +186 -0
- package/runtime/tracing/span.ts +138 -0
- package/runtime/tracing/store.ts +499 -0
- package/runtime/tracing/types.ts +47 -0
- package/runtime/vite-plugin/config-plugin.ts +68 -0
- package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
- package/runtime/vite-plugin/dist/index.mjs +52333 -0
- package/runtime/vite-plugin/globals-plugin.ts +94 -0
- package/runtime/vite-plugin/index.ts +43 -0
- package/runtime/vite-plugin/modules-plugin.ts +88 -0
- package/runtime/vite-plugin/react-router-plugin.ts +95 -0
- package/runtime/worker-registry.ts +52 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { useState, useEffect } from "preact/hooks";
|
|
2
|
+
import { useMutation } from "../rpc/hooks";
|
|
3
|
+
import type { ErrorSummary, ErrorDetail, SpanData } from "../rpc/types";
|
|
4
|
+
import { rpc } from "../rpc/client";
|
|
5
|
+
import { navigate } from "../lib";
|
|
6
|
+
import { formatDuration } from "./trace-waterfall";
|
|
7
|
+
import { KeyValueTable } from "../components/key-value-table";
|
|
8
|
+
import { RefreshButton } from "../components";
|
|
9
|
+
|
|
10
|
+
export function ErrorsView({ route }: { route: string }) {
|
|
11
|
+
const parts = route.split("/").filter(Boolean);
|
|
12
|
+
// /errors → list, /errors/{id} → detail
|
|
13
|
+
if (parts.length >= 2) return <ErrorDetailPage errorId={decodeURIComponent(parts[1]!)} />;
|
|
14
|
+
return <ErrorList />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─── Error List ──────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function ErrorList() {
|
|
20
|
+
const [errors, setErrors] = useState<ErrorSummary[]>([]);
|
|
21
|
+
const [cursor, setCursor] = useState<string | null>(null);
|
|
22
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
23
|
+
const clearErrors = useMutation("errors.clear");
|
|
24
|
+
|
|
25
|
+
const loadErrors = (cur?: string) => {
|
|
26
|
+
setIsLoading(true);
|
|
27
|
+
rpc("errors.list", { limit: 50, cursor: cur }).then(data => {
|
|
28
|
+
if (cur) {
|
|
29
|
+
setErrors(prev => [...prev, ...data.items]);
|
|
30
|
+
} else {
|
|
31
|
+
setErrors(data.items);
|
|
32
|
+
}
|
|
33
|
+
setCursor(data.cursor);
|
|
34
|
+
setIsLoading(false);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
useEffect(() => { loadErrors(); }, []);
|
|
39
|
+
|
|
40
|
+
const handleClear = () => {
|
|
41
|
+
clearErrors.mutate().then(() => {
|
|
42
|
+
setErrors([]);
|
|
43
|
+
setCursor(null);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleDelete = (id: string, e: Event) => {
|
|
48
|
+
e.stopPropagation();
|
|
49
|
+
rpc("errors.delete", { id }).then(() => {
|
|
50
|
+
setErrors(prev => prev.filter(err => err.id !== id));
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div class="p-8 h-full flex flex-col">
|
|
56
|
+
<div class="flex items-center justify-between mb-6">
|
|
57
|
+
<div>
|
|
58
|
+
<h1 class="text-2xl font-bold text-ink">Errors</h1>
|
|
59
|
+
<p class="text-sm text-text-muted mt-1">{errors.length} error(s)</p>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="flex gap-2 items-center">
|
|
62
|
+
<RefreshButton onClick={() => loadErrors()} />
|
|
63
|
+
{errors.length > 0 && (
|
|
64
|
+
<button
|
|
65
|
+
onClick={handleClear}
|
|
66
|
+
class="rounded-md px-3 py-1.5 text-sm font-medium bg-panel border border-border text-text-secondary btn-danger transition-all"
|
|
67
|
+
>
|
|
68
|
+
Clear all
|
|
69
|
+
</button>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="flex-1 overflow-y-auto scrollbar-thin">
|
|
75
|
+
{errors.length === 0 && !isLoading ? (
|
|
76
|
+
<div class="text-text-muted font-medium text-center py-12">No errors recorded yet.</div>
|
|
77
|
+
) : (
|
|
78
|
+
<div class="bg-panel rounded-lg border border-border overflow-hidden">
|
|
79
|
+
<table class="w-full text-sm">
|
|
80
|
+
<thead>
|
|
81
|
+
<tr class="border-b border-border-subtle">
|
|
82
|
+
<th class="text-left px-4 py-2.5 text-xs text-text-muted font-medium">Source</th>
|
|
83
|
+
<th class="text-left px-4 py-2.5 text-xs text-text-muted font-medium">Error</th>
|
|
84
|
+
<th class="text-left px-4 py-2.5 text-xs text-text-muted font-medium">Message</th>
|
|
85
|
+
<th class="text-left px-4 py-2.5 text-xs text-text-muted font-medium">Context</th>
|
|
86
|
+
<th class="text-left px-4 py-2.5 text-xs text-text-muted font-medium">Worker</th>
|
|
87
|
+
<th class="text-right px-4 py-2.5 text-xs text-text-muted font-medium">Trace</th>
|
|
88
|
+
<th class="text-right px-4 py-2.5 text-xs text-text-muted font-medium">Time</th>
|
|
89
|
+
<th class="text-right px-4 py-2.5 text-xs text-text-muted font-medium w-10"></th>
|
|
90
|
+
</tr>
|
|
91
|
+
</thead>
|
|
92
|
+
<tbody>
|
|
93
|
+
{errors.map(err => (
|
|
94
|
+
<tr
|
|
95
|
+
key={err.id}
|
|
96
|
+
onClick={() => navigate(`/errors/${err.id}`)}
|
|
97
|
+
class="border-b border-border-row cursor-pointer transition-colors hover:bg-panel-hover/50"
|
|
98
|
+
>
|
|
99
|
+
<td class="px-4 py-2.5">
|
|
100
|
+
<SourceBadge source={err.source} />
|
|
101
|
+
</td>
|
|
102
|
+
<td class="px-4 py-2.5 font-medium text-ink">{err.errorName}</td>
|
|
103
|
+
<td class="px-4 py-2.5 text-text-data truncate max-w-[250px]">{err.errorMessage}</td>
|
|
104
|
+
<td class="px-4 py-2.5 text-text-secondary font-mono text-xs">
|
|
105
|
+
{err.requestMethod && err.requestUrl ? (
|
|
106
|
+
<span>
|
|
107
|
+
<span class="font-medium">{err.requestMethod}</span>{" "}
|
|
108
|
+
{truncateUrl(err.requestUrl)}
|
|
109
|
+
</span>
|
|
110
|
+
) : (
|
|
111
|
+
<span class="text-text-dim">-</span>
|
|
112
|
+
)}
|
|
113
|
+
</td>
|
|
114
|
+
<td class="px-4 py-2.5">
|
|
115
|
+
{err.workerName && (
|
|
116
|
+
<span class="inline-flex px-2 py-0.5 rounded-md text-xs font-medium bg-panel-hover text-text-secondary">
|
|
117
|
+
{err.workerName}
|
|
118
|
+
</span>
|
|
119
|
+
)}
|
|
120
|
+
</td>
|
|
121
|
+
<td class="px-4 py-2.5 text-right">
|
|
122
|
+
{err.traceId ? (
|
|
123
|
+
<a
|
|
124
|
+
href={`#/traces?trace=${err.traceId}`}
|
|
125
|
+
onClick={(e) => e.stopPropagation()}
|
|
126
|
+
class="text-blue-500 hover:text-blue-700 text-xs font-mono"
|
|
127
|
+
>
|
|
128
|
+
{err.traceId.slice(0, 8)}...
|
|
129
|
+
</a>
|
|
130
|
+
) : (
|
|
131
|
+
<span class="text-text-dim text-xs">-</span>
|
|
132
|
+
)}
|
|
133
|
+
</td>
|
|
134
|
+
<td class="px-4 py-2.5 text-right font-mono text-xs text-text-muted">
|
|
135
|
+
{formatTimestamp(err.timestamp)}
|
|
136
|
+
</td>
|
|
137
|
+
<td class="px-4 py-2.5 text-right">
|
|
138
|
+
<button
|
|
139
|
+
onClick={(e) => handleDelete(err.id, e)}
|
|
140
|
+
class="text-text-dim hover:text-red-500 transition-colors"
|
|
141
|
+
title="Delete error"
|
|
142
|
+
>
|
|
143
|
+
×
|
|
144
|
+
</button>
|
|
145
|
+
</td>
|
|
146
|
+
</tr>
|
|
147
|
+
))}
|
|
148
|
+
</tbody>
|
|
149
|
+
</table>
|
|
150
|
+
{cursor && (
|
|
151
|
+
<div class="p-4 text-center border-t border-border-subtle">
|
|
152
|
+
<button
|
|
153
|
+
onClick={() => loadErrors(cursor)}
|
|
154
|
+
disabled={isLoading}
|
|
155
|
+
class="text-sm text-text-secondary hover:text-ink disabled:text-text-dim"
|
|
156
|
+
>
|
|
157
|
+
{isLoading ? "Loading..." : "Load more"}
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
{isLoading && errors.length === 0 && (
|
|
164
|
+
<div class="text-text-muted text-sm text-center py-12">Loading errors...</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── Error Detail Page ───────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
function ErrorDetailPage({ errorId }: { errorId: string }) {
|
|
174
|
+
const [detail, setDetail] = useState<ErrorDetail | null>(null);
|
|
175
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
176
|
+
const [traceSpans, setTraceSpans] = useState<SpanData[] | null>(null);
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
setIsLoading(true);
|
|
180
|
+
setTraceSpans(null);
|
|
181
|
+
rpc("errors.get", { id: errorId }).then(data => {
|
|
182
|
+
const d = data as ErrorDetail;
|
|
183
|
+
setDetail(d);
|
|
184
|
+
setIsLoading(false);
|
|
185
|
+
if (d.traceId) {
|
|
186
|
+
rpc("traces.getTrace", { traceId: d.traceId }).then(trace => {
|
|
187
|
+
setTraceSpans(trace.spans);
|
|
188
|
+
}).catch(() => {});
|
|
189
|
+
}
|
|
190
|
+
}).catch(() => setIsLoading(false));
|
|
191
|
+
}, [errorId]);
|
|
192
|
+
|
|
193
|
+
const handleDelete = () => {
|
|
194
|
+
rpc("errors.delete", { id: errorId }).then(() => {
|
|
195
|
+
navigate("/errors");
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (isLoading) {
|
|
200
|
+
return <div class="p-8 text-text-muted text-sm">Loading error details...</div>;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!detail) {
|
|
204
|
+
return <div class="p-8 text-text-muted text-sm">Error not found.</div>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const { data } = detail;
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div class="p-6 max-w-6xl flex flex-col gap-4">
|
|
211
|
+
{/* Breadcrumb + actions */}
|
|
212
|
+
<div class="flex items-center justify-between">
|
|
213
|
+
<div class="flex items-center gap-2 text-sm text-text-muted">
|
|
214
|
+
<a href="#/errors" class="text-text-secondary hover:text-ink no-underline font-medium transition-colors">Errors</a>
|
|
215
|
+
<span class="text-text-dim">/</span>
|
|
216
|
+
<span class="text-ink font-semibold">{errorId.slice(0, 12)}...</span>
|
|
217
|
+
</div>
|
|
218
|
+
<button
|
|
219
|
+
onClick={handleDelete}
|
|
220
|
+
class="rounded-md px-3 py-1.5 text-sm font-medium bg-panel border border-border text-text-secondary btn-danger transition-all"
|
|
221
|
+
>
|
|
222
|
+
Delete
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{/* Error header */}
|
|
227
|
+
<div class="bg-panel rounded-lg border border-border overflow-hidden border-l-4 border-l-red-500">
|
|
228
|
+
<div class="px-5 py-4">
|
|
229
|
+
<div class="flex items-center gap-2.5 mb-1.5">
|
|
230
|
+
<span class="w-6 h-6 rounded-md flex items-center justify-center text-xs font-bold" style={{ background: "var(--color-error-icon-bg)", color: "var(--color-error-icon-text)" }}>!</span>
|
|
231
|
+
<span class="text-xs font-semibold uppercase tracking-wider text-red-500">{data.error.name}</span>
|
|
232
|
+
{detail.source && <SourceBadge source={detail.source} />}
|
|
233
|
+
</div>
|
|
234
|
+
<CollapsibleMessage message={data.error.message} />
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
{/* Source Code */}
|
|
239
|
+
{data.error.frames.length > 0 && (
|
|
240
|
+
<Section title="Source Code" open>
|
|
241
|
+
<FrameList frames={data.error.frames} />
|
|
242
|
+
</Section>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{/* Stack Trace */}
|
|
246
|
+
<Section title="Stack Trace">
|
|
247
|
+
<div class="px-4 py-3 overflow-x-auto scrollbar-thin">
|
|
248
|
+
<pre class="text-xs text-text-secondary leading-5 m-0 whitespace-pre-wrap break-words font-mono">
|
|
249
|
+
{data.error.stack}
|
|
250
|
+
</pre>
|
|
251
|
+
</div>
|
|
252
|
+
</Section>
|
|
253
|
+
|
|
254
|
+
{/* Trace */}
|
|
255
|
+
{traceSpans && traceSpans.length > 0 && (
|
|
256
|
+
<Section title="Trace" open>
|
|
257
|
+
<SimpleTraceWaterfall spans={traceSpans} highlightSpanId={detail.spanId} />
|
|
258
|
+
</Section>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{/* Request */}
|
|
262
|
+
{data.request.method && data.request.url && (
|
|
263
|
+
<Section title="Request" open>
|
|
264
|
+
<div class="px-4 py-2.5 border-b border-border-subtle">
|
|
265
|
+
<span class="inline-block px-2 py-0.5 rounded-md bg-panel-hover text-xs font-bold mr-2">{data.request.method}</span>
|
|
266
|
+
<span class="text-sm break-all font-mono">{data.request.url}</span>
|
|
267
|
+
</div>
|
|
268
|
+
<KeyValueTable data={data.request.headers} />
|
|
269
|
+
</Section>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{/* Environment */}
|
|
273
|
+
{Object.keys(data.env).length > 0 && (
|
|
274
|
+
<Section title="Environment">
|
|
275
|
+
<KeyValueTable data={data.env} />
|
|
276
|
+
</Section>
|
|
277
|
+
)}
|
|
278
|
+
|
|
279
|
+
{/* Bindings */}
|
|
280
|
+
{data.bindings.length > 0 && (
|
|
281
|
+
<Section title="Bindings">
|
|
282
|
+
{data.bindings.length === 0 ? (
|
|
283
|
+
<div class="px-4 py-3 text-sm text-text-muted">No bindings configured</div>
|
|
284
|
+
) : (
|
|
285
|
+
<table class="w-full text-sm">
|
|
286
|
+
<tbody>
|
|
287
|
+
{data.bindings.map((b) => (
|
|
288
|
+
<tr key={b.name} class="border-b border-border-subtle last:border-0 hover:bg-panel-hover/50 transition-colors">
|
|
289
|
+
<td class="px-4 py-2 font-medium text-text-secondary whitespace-nowrap font-mono">
|
|
290
|
+
{b.name}
|
|
291
|
+
</td>
|
|
292
|
+
<td class="px-4 py-2">
|
|
293
|
+
<span class="inline-block px-2 py-0.5 rounded-md bg-panel-hover text-xs font-medium text-text-data">{b.type}</span>
|
|
294
|
+
</td>
|
|
295
|
+
</tr>
|
|
296
|
+
))}
|
|
297
|
+
</tbody>
|
|
298
|
+
</table>
|
|
299
|
+
)}
|
|
300
|
+
</Section>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
{/* Runtime */}
|
|
304
|
+
<Section title="Runtime">
|
|
305
|
+
<KeyValueTable
|
|
306
|
+
data={{
|
|
307
|
+
"Bun": data.runtime.bunVersion,
|
|
308
|
+
"Platform": `${data.runtime.platform} / ${data.runtime.arch}`,
|
|
309
|
+
...(data.runtime.workerName ? { "Worker": data.runtime.workerName } : {}),
|
|
310
|
+
...(data.runtime.configName ? { "Config": data.runtime.configName } : {}),
|
|
311
|
+
}}
|
|
312
|
+
/>
|
|
313
|
+
</Section>
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ─── Simplified Trace Waterfall (matches standalone error page) ──────
|
|
319
|
+
|
|
320
|
+
function SimpleTraceWaterfall({ spans, highlightSpanId }: { spans: SpanData[]; highlightSpanId: string | null }) {
|
|
321
|
+
if (spans.length === 0) return null;
|
|
322
|
+
|
|
323
|
+
const traceStart = Math.min(...spans.map(s => s.startTime));
|
|
324
|
+
const traceEnd = Math.max(...spans.map(s => s.endTime ?? Date.now()));
|
|
325
|
+
const traceDuration = traceEnd - traceStart || 1;
|
|
326
|
+
|
|
327
|
+
const childMap = new Map<string | null, SpanData[]>();
|
|
328
|
+
for (const s of spans) {
|
|
329
|
+
const key = s.parentSpanId;
|
|
330
|
+
if (!childMap.has(key)) childMap.set(key, []);
|
|
331
|
+
childMap.get(key)!.push(s);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function flatten(parentId: string | null, depth: number): Array<{ span: SpanData; depth: number }> {
|
|
335
|
+
const children = childMap.get(parentId) ?? [];
|
|
336
|
+
const result: Array<{ span: SpanData; depth: number }> = [];
|
|
337
|
+
for (const child of children) {
|
|
338
|
+
result.push({ span: child, depth });
|
|
339
|
+
result.push(...flatten(child.spanId, depth + 1));
|
|
340
|
+
}
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const flatSpans = flatten(null, 0);
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div class="px-4 py-3">
|
|
348
|
+
<div class="flex items-center justify-between mb-2">
|
|
349
|
+
<span class="text-xs text-text-muted font-mono">0ms</span>
|
|
350
|
+
<span class="text-xs text-text-muted font-mono">{formatDuration(traceDuration)}</span>
|
|
351
|
+
</div>
|
|
352
|
+
<div class="space-y-0.5">
|
|
353
|
+
{flatSpans.map(({ span, depth }) => {
|
|
354
|
+
const offset = ((span.startTime - traceStart) / traceDuration) * 100;
|
|
355
|
+
const width = (((span.endTime ?? Date.now()) - span.startTime) / traceDuration) * 100;
|
|
356
|
+
const isHighlighted = highlightSpanId === span.spanId;
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<div
|
|
360
|
+
key={span.spanId}
|
|
361
|
+
class="flex items-center py-1 px-1 rounded-md"
|
|
362
|
+
style={isHighlighted ? { background: "var(--color-error-highlight)", boxShadow: `inset 0 0 0 1px var(--color-error-ring)` } : undefined}
|
|
363
|
+
>
|
|
364
|
+
<div
|
|
365
|
+
class="w-[180px] flex-shrink-0 truncate text-xs text-ink font-mono"
|
|
366
|
+
style={{ paddingLeft: `${depth * 14}px` }}
|
|
367
|
+
>
|
|
368
|
+
{span.name}
|
|
369
|
+
</div>
|
|
370
|
+
<div class="flex-1 h-5 relative bg-panel-secondary rounded">
|
|
371
|
+
<div
|
|
372
|
+
class="absolute top-0.5 bottom-0.5 rounded"
|
|
373
|
+
style={{ background: span.status === "error" ? "var(--color-span-error)" : span.status === "ok" ? "var(--color-span-ok)" : "#d1d5db", left: `${offset}%`, width: `${Math.max(width, 0.5)}%` }}
|
|
374
|
+
/>
|
|
375
|
+
<span
|
|
376
|
+
class="absolute top-0.5 text-[10px] text-text-secondary whitespace-nowrap font-mono"
|
|
377
|
+
style={{ left: `${offset + width + 1}%` }}
|
|
378
|
+
>
|
|
379
|
+
{span.durationMs != null ? formatDuration(span.durationMs) : "..."}
|
|
380
|
+
</span>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
})}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ─── Shared Components (matching standalone error page style) ────────
|
|
391
|
+
|
|
392
|
+
function Section({ title, open, children }: { title: string; open?: boolean; children: any }) {
|
|
393
|
+
return (
|
|
394
|
+
<details open={open} class="bg-panel rounded-lg border border-border overflow-hidden">
|
|
395
|
+
<summary class="px-5 py-3 cursor-pointer select-none text-sm font-semibold text-ink hover:bg-panel-hover transition-colors">
|
|
396
|
+
{title}
|
|
397
|
+
</summary>
|
|
398
|
+
<div class="border-t border-border-subtle">
|
|
399
|
+
{children}
|
|
400
|
+
</div>
|
|
401
|
+
</details>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
type FrameData = ErrorDetail["data"]["error"]["frames"][0];
|
|
406
|
+
|
|
407
|
+
const LIBRARY_PATH_RE = /\/node_modules\//;
|
|
408
|
+
|
|
409
|
+
function isLibraryFrame(frame: FrameData): boolean {
|
|
410
|
+
return LIBRARY_PATH_RE.test(frame.file);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const HL_RE = /(\/\/.*$|\/\*.*?\*\/)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b)|(\b(?:const|let|var|function|return|if|else|for|while|do|class|new|import|export|from|default|async|await|throw|try|catch|finally|switch|case|break|continue|typeof|instanceof|in|of|yield|static|extends|super|void|delete|enum|interface|type|as|declare|readonly)\b)|(\b(?:true|false|null|undefined|this|NaN|Infinity)\b)/g;
|
|
414
|
+
|
|
415
|
+
function highlightLine(line: string) {
|
|
416
|
+
const parts: preact.ComponentChildren[] = [];
|
|
417
|
+
let last = 0;
|
|
418
|
+
for (const m of line.matchAll(HL_RE)) {
|
|
419
|
+
if (m.index! > last) parts.push(line.slice(last, m.index));
|
|
420
|
+
const t = m[0];
|
|
421
|
+
const c = m[1] ? "#6b7280" : m[2] ? "#16a34a" : m[3] ? "#d97706" : m[4] ? "#7c3aed" : m[5] ? "#2563eb" : undefined;
|
|
422
|
+
parts.push(c ? <span style={{ color: c }}>{t}</span> : t);
|
|
423
|
+
last = m.index! + t.length;
|
|
424
|
+
}
|
|
425
|
+
if (last < line.length) parts.push(line.slice(last));
|
|
426
|
+
return parts.length > 0 ? parts : line;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function FrameList({ frames }: { frames: FrameData[] }) {
|
|
430
|
+
return (
|
|
431
|
+
<div class="divide-y divide-border-subtle">
|
|
432
|
+
{frames.map((frame, i) => (
|
|
433
|
+
<CodeBlock key={i} frame={frame} defaultOpen={!isLibraryFrame(frame)} />
|
|
434
|
+
))}
|
|
435
|
+
</div>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function CodeBlock({ frame, defaultOpen }: { frame: FrameData; defaultOpen: boolean }) {
|
|
440
|
+
if (!frame.source || frame.source.length === 0) return null;
|
|
441
|
+
const startLine = frame.line - (frame.sourceLine ?? 0);
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<details open={defaultOpen}>
|
|
445
|
+
<summary class="px-4 py-2 bg-panel-secondary text-xs font-medium text-text-secondary font-mono cursor-pointer select-none hover:bg-panel-hover transition-colors">
|
|
446
|
+
{frame.file}:{frame.line}:{frame.column}
|
|
447
|
+
{frame.function && <span class="ml-2 text-text-muted">in {frame.function}</span>}
|
|
448
|
+
</summary>
|
|
449
|
+
<div class="overflow-x-auto scrollbar-thin">
|
|
450
|
+
<pre class="text-xs leading-5 m-0 font-mono">
|
|
451
|
+
{frame.source.map((line, i) => {
|
|
452
|
+
const lineNum = startLine + i;
|
|
453
|
+
const isError = i === frame.sourceLine;
|
|
454
|
+
return (
|
|
455
|
+
<div
|
|
456
|
+
key={i}
|
|
457
|
+
class={isError ? "border-l-4 border-red-500" : "border-l-4 border-transparent hover:bg-panel-hover"}
|
|
458
|
+
style={isError ? { background: "var(--color-error-highlight)" } : undefined}
|
|
459
|
+
>
|
|
460
|
+
<span class={`inline-block w-12 text-right pr-3 select-none ${isError ? "text-red-500 font-bold" : "text-text-muted"}`}>
|
|
461
|
+
{lineNum}
|
|
462
|
+
</span>
|
|
463
|
+
<span class={`text-ink${isError ? " font-medium" : ""}`}>{highlightLine(line)}</span>
|
|
464
|
+
</div>
|
|
465
|
+
);
|
|
466
|
+
})}
|
|
467
|
+
</pre>
|
|
468
|
+
</div>
|
|
469
|
+
</details>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
const MAX_COLLAPSED_LINES = 10;
|
|
475
|
+
|
|
476
|
+
function CollapsibleMessage({ message }: { message: string }) {
|
|
477
|
+
const [expanded, setExpanded] = useState(false);
|
|
478
|
+
const nlIndex = message.indexOf("\n");
|
|
479
|
+
|
|
480
|
+
if (nlIndex === -1) {
|
|
481
|
+
return <h1 class="text-lg font-bold text-ink m-0 leading-snug break-words">{message}</h1>;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const firstLine = message.slice(0, nlIndex);
|
|
485
|
+
const rest = message.slice(nlIndex + 1);
|
|
486
|
+
const restLines = rest.split("\n");
|
|
487
|
+
const needsCollapse = restLines.length > MAX_COLLAPSED_LINES;
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<>
|
|
491
|
+
<h1 class="text-lg font-bold text-ink m-0 leading-snug break-words">{firstLine}</h1>
|
|
492
|
+
<div class="relative mt-2">
|
|
493
|
+
<pre
|
|
494
|
+
class="text-xs text-text-secondary m-0 whitespace-pre-wrap break-words leading-5 overflow-hidden transition-all font-mono"
|
|
495
|
+
style={{
|
|
496
|
+
maxHeight: !expanded && needsCollapse ? `${MAX_COLLAPSED_LINES * 1.25}rem` : "none",
|
|
497
|
+
}}
|
|
498
|
+
>
|
|
499
|
+
{rest}
|
|
500
|
+
</pre>
|
|
501
|
+
{needsCollapse && !expanded && (
|
|
502
|
+
<div
|
|
503
|
+
class="absolute bottom-0 left-0 right-0 h-16 flex items-end justify-center pb-2 cursor-pointer"
|
|
504
|
+
style="background: linear-gradient(to bottom, transparent, var(--color-panel));"
|
|
505
|
+
onClick={() => setExpanded(true)}
|
|
506
|
+
>
|
|
507
|
+
<span class="text-xs font-medium text-text-muted hover:text-ink transition-colors">
|
|
508
|
+
Show all ({restLines.length} lines)
|
|
509
|
+
</span>
|
|
510
|
+
</div>
|
|
511
|
+
)}
|
|
512
|
+
{needsCollapse && expanded && (
|
|
513
|
+
<button
|
|
514
|
+
class="mt-1 text-xs font-medium text-text-muted hover:text-ink transition-colors"
|
|
515
|
+
onClick={() => setExpanded(false)}
|
|
516
|
+
>
|
|
517
|
+
Collapse
|
|
518
|
+
</button>
|
|
519
|
+
)}
|
|
520
|
+
</div>
|
|
521
|
+
</>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
526
|
+
|
|
527
|
+
const SOURCE_COLORS: Record<string, { bg: string; text: string }> = {
|
|
528
|
+
fetch: { bg: "var(--color-badge-blue-bg)", text: "var(--color-badge-blue-text)" },
|
|
529
|
+
scheduled: { bg: "var(--color-badge-purple-bg)", text: "var(--color-badge-purple-text)" },
|
|
530
|
+
queue: { bg: "var(--color-badge-orange-bg)", text: "var(--color-badge-orange-text)" },
|
|
531
|
+
alarm: { bg: "var(--color-badge-yellow-bg)", text: "var(--color-badge-yellow-text)" },
|
|
532
|
+
workflow: { bg: "var(--color-badge-emerald-bg)", text: "var(--color-badge-emerald-text)" },
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
function SourceBadge({ source }: { source: string | null }) {
|
|
536
|
+
const label = source ?? "unknown";
|
|
537
|
+
const color = SOURCE_COLORS[label] ?? { bg: "var(--color-badge-red-bg)", text: "var(--color-badge-red-text)" };
|
|
538
|
+
return (
|
|
539
|
+
<span class="inline-flex px-2 py-0.5 rounded-md text-xs font-medium" style={{ background: color.bg, color: color.text }}>
|
|
540
|
+
{label}
|
|
541
|
+
</span>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function formatTimestamp(ts: number): string {
|
|
546
|
+
const d = new Date(ts);
|
|
547
|
+
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}.${d.getMilliseconds().toString().padStart(3, "0")}`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function truncateUrl(url: string): string {
|
|
551
|
+
try {
|
|
552
|
+
const u = new URL(url);
|
|
553
|
+
const path = u.pathname + u.search;
|
|
554
|
+
return path.length > 50 ? path.slice(0, 50) + "..." : path;
|
|
555
|
+
} catch {
|
|
556
|
+
return url.length > 50 ? url.slice(0, 50) + "..." : url;
|
|
557
|
+
}
|
|
558
|
+
}
|