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,410 @@
1
+ import { useState, useEffect } from "preact/hooks";
2
+ import type { SpanData, SpanEventData } from "../rpc/types";
3
+
4
+ // ─── Types ───────────────────────────────────────────────────────────
5
+
6
+ export interface TraceWaterfallProps {
7
+ spans: SpanData[];
8
+ events: SpanEventData[];
9
+ highlightSpanId?: string | null;
10
+ onAddAttributeFilter?: (key: string, value: string, type: "include" | "exclude") => void;
11
+ }
12
+
13
+ // ─── Component ───────────────────────────────────────────────────────
14
+
15
+ export function TraceWaterfall({ spans, events, highlightSpanId, onAddAttributeFilter }: TraceWaterfallProps) {
16
+ const [expandedSpan, setExpandedSpan] = useState<string | null>(null);
17
+ const [collapsedSpans, setCollapsedSpans] = useState<Set<string>>(new Set());
18
+
19
+ // Compute waterfall layout
20
+ const traceStart = spans.length > 0 ? Math.min(...spans.map(s => s.startTime)) : 0;
21
+ const traceEnd = spans.length > 0 ? Math.max(...spans.map(s => (s.endTime ?? Date.now()))) : 0;
22
+ const traceDuration = traceEnd - traceStart || 1;
23
+
24
+ // Build tree structure
25
+ const spanMap = new Map(spans.map(s => [s.spanId, s]));
26
+ const childMap = new Map<string | null, SpanData[]>();
27
+ for (const s of spans) {
28
+ const key = s.parentSpanId;
29
+ if (!childMap.has(key)) childMap.set(key, []);
30
+ childMap.get(key)!.push(s);
31
+ }
32
+
33
+ // Auto-expand: first 2 levels OR spans with >10% duration
34
+ const getAutoExpanded = (): Set<string> => {
35
+ const autoCollapsed = new Set<string>();
36
+ function walk(parentId: string | null, depth: number) {
37
+ const children = childMap.get(parentId) ?? [];
38
+ for (const child of children) {
39
+ const hasChildren = (childMap.get(child.spanId) ?? []).length > 0;
40
+ if (hasChildren) {
41
+ const spanDur = child.durationMs ?? 0;
42
+ const significantDuration = spanDur > traceDuration * 0.1;
43
+ if (depth >= 2 && !significantDuration) {
44
+ autoCollapsed.add(child.spanId);
45
+ }
46
+ }
47
+ walk(child.spanId, depth + 1);
48
+ }
49
+ }
50
+ walk(null, 0);
51
+ return autoCollapsed;
52
+ };
53
+
54
+ // Initialize collapsed state when spans change
55
+ useEffect(() => {
56
+ if (spans.length > 0) {
57
+ setCollapsedSpans(getAutoExpanded());
58
+ setExpandedSpan(null);
59
+ }
60
+ }, [spans]);
61
+
62
+ const toggleCollapse = (spanId: string) => {
63
+ setCollapsedSpans(prev => {
64
+ const next = new Set(prev);
65
+ if (next.has(spanId)) {
66
+ next.delete(spanId);
67
+ } else {
68
+ next.add(spanId);
69
+ }
70
+ return next;
71
+ });
72
+ };
73
+
74
+ function flattenTree(parentId: string | null, depth: number): Array<{ span: SpanData; depth: number }> {
75
+ const children = childMap.get(parentId) ?? [];
76
+ const result: Array<{ span: SpanData; depth: number }> = [];
77
+ for (const child of children) {
78
+ result.push({ span: child, depth });
79
+ if (!collapsedSpans.has(child.spanId)) {
80
+ result.push(...flattenTree(child.spanId, depth + 1));
81
+ }
82
+ }
83
+ return result;
84
+ }
85
+
86
+ const flatSpans = flattenTree(null, 0);
87
+
88
+ // Get parent span attributes for filtering inherited attrs
89
+ const getParentAttributes = (span: SpanData): Record<string, unknown> => {
90
+ if (!span.parentSpanId) return {};
91
+ const parent = spanMap.get(span.parentSpanId);
92
+ return parent?.attributes ?? {};
93
+ };
94
+
95
+ return (
96
+ <div>
97
+ {/* Timeline header */}
98
+ <div class="flex items-center justify-between mb-3">
99
+ <span class="text-xs text-text-muted font-mono">0ms</span>
100
+ <span class="text-xs text-text-muted font-mono">{formatDuration(traceDuration)}</span>
101
+ </div>
102
+
103
+ {/* Waterfall */}
104
+ <div class="space-y-0.5">
105
+ {flatSpans.map(({ span, depth }) => {
106
+ const offset = ((span.startTime - traceStart) / traceDuration) * 100;
107
+ const width = (((span.endTime ?? Date.now()) - span.startTime) / traceDuration) * 100;
108
+ const spanEvents = events.filter(e => e.spanId === span.spanId);
109
+ const isExpanded = expandedSpan === span.spanId;
110
+ const hasChildren = (childMap.get(span.spanId) ?? []).length > 0;
111
+ const isCollapsed = collapsedSpans.has(span.spanId);
112
+ const parentAttrs = getParentAttributes(span);
113
+ const isHighlighted = highlightSpanId === span.spanId;
114
+
115
+ // Key attributes to show in the bar
116
+ const keyAttrs = width > 5 ? getKeyAttributes(span.attributes, 2) : [];
117
+
118
+ return (
119
+ <div key={span.spanId}>
120
+ <div
121
+ class={`flex items-center cursor-pointer hover:bg-panel-hover rounded-md py-1 px-1 transition-colors`}
122
+ style={isHighlighted ? { background: "var(--color-error-highlight)", boxShadow: `inset 0 0 0 2px var(--color-span-error)` } : undefined}
123
+ onClick={() => setExpandedSpan(isExpanded ? null : span.spanId)}
124
+ >
125
+ {/* Span name with collapse toggle */}
126
+ <div class="w-[200px] flex-shrink-0 truncate text-xs text-ink flex items-center" style={{ paddingLeft: `${depth * 16}px` }}>
127
+ {hasChildren && (
128
+ <span
129
+ class="inline-block w-4 text-text-muted cursor-pointer select-none flex-shrink-0"
130
+ onClick={(e) => { e.stopPropagation(); toggleCollapse(span.spanId); }}
131
+ >
132
+ {isCollapsed ? "\u25B6" : "\u25BC"}
133
+ </span>
134
+ )}
135
+ {!hasChildren && <span class="inline-block w-4 flex-shrink-0" />}
136
+ <span class="truncate">{span.name}</span>
137
+ </div>
138
+ {/* Bar area */}
139
+ <div class="flex-1 h-6 relative bg-panel-secondary rounded">
140
+ <div
141
+ class={`absolute top-0.5 bottom-0.5 rounded flex items-center overflow-hidden ${
142
+ span.status !== "error" && span.status !== "ok" ? "animate-pulse" : ""
143
+ }`}
144
+ style={{
145
+ left: `${offset}%`, width: `${Math.max(width, 0.5)}%`,
146
+ background: span.status === "error" ? "var(--color-span-error)" : span.status === "ok" ? "var(--color-span-ok)" : "#d1d5db",
147
+ }}
148
+ >
149
+ {/* Key attributes inside bar */}
150
+ {keyAttrs.length > 0 && (
151
+ <span class="text-[9px] text-white px-1 truncate whitespace-nowrap">
152
+ {keyAttrs.map(([k, v]) => `${k}=${String(v)}`).join(" ")}
153
+ </span>
154
+ )}
155
+ {/* Event markers */}
156
+ {spanEvents.map(ev => {
157
+ const evOffset = ((ev.timestamp - span.startTime) / ((span.endTime ?? Date.now()) - span.startTime || 1)) * 100;
158
+ return (
159
+ <div
160
+ key={ev.id}
161
+ class={`absolute top-0 w-1.5 h-full rounded-full ${ev.name === "exception" ? "bg-red-600" : "bg-panel-secondary0"}`}
162
+ style={{ left: `${Math.min(evOffset, 100)}%` }}
163
+ title={ev.message ?? ev.name}
164
+ />
165
+ );
166
+ })}
167
+ </div>
168
+ {/* Duration label */}
169
+ <span
170
+ class="absolute top-0.5 text-[10px] text-text-secondary whitespace-nowrap"
171
+ style={{ left: `${offset + width + 1}%` }}
172
+ >
173
+ {span.durationMs !== null ? formatDuration(span.durationMs) : "..."}
174
+ </span>
175
+ </div>
176
+ </div>
177
+
178
+ {/* Expanded detail */}
179
+ {isExpanded && (
180
+ <div class="bg-panel-secondary border border-border-subtle rounded-lg p-4 mt-1 mb-2 ml-4">
181
+ <div class="text-xs space-y-3">
182
+ {/* Timing section */}
183
+ <div class="grid grid-cols-[auto_1fr_auto_1fr] gap-x-3 gap-y-1.5 items-baseline">
184
+ <span class="text-text-muted">Kind:</span> <span class="text-ink">{span.kind}</span>
185
+ <span class="text-text-muted">Status:</span> <TraceStatusBadge status={span.status} />
186
+ <span class="text-text-muted">Start:</span> <span class="text-ink font-mono">{formatTimestamp(span.startTime)}</span>
187
+ <span class="text-text-muted">End:</span> <span class="text-ink font-mono">{span.endTime ? formatTimestamp(span.endTime) : "..."}</span>
188
+ <span class="text-text-muted">Duration:</span> <span class="text-ink font-mono">{span.durationMs !== null ? formatDuration(span.durationMs) : "..."}</span>
189
+ <span class="text-text-muted">Trace ID:</span> <span class="text-ink font-mono">{span.traceId.slice(0, 16)}...</span>
190
+ {span.parentSpanId && (
191
+ <>
192
+ <span class="text-text-muted">Parent:</span> <span class="text-ink font-mono">{span.parentSpanId.slice(0, 16)}...</span>
193
+ </>
194
+ )}
195
+ </div>
196
+ {span.statusMessage && (
197
+ <div>
198
+ <span class="text-text-muted">Error:</span>
199
+ <span class="ml-2 text-red-500">{span.statusMessage}</span>
200
+ </div>
201
+ )}
202
+ {/* Attributes (filtered: inherited removed) */}
203
+ {Object.keys(span.attributes).length > 0 && (() => {
204
+ const filteredAttrs = Object.entries(span.attributes).filter(([k, v]) => {
205
+ const parentVal = parentAttrs[k];
206
+ return parentVal === undefined || JSON.stringify(parentVal) !== JSON.stringify(v);
207
+ });
208
+ if (filteredAttrs.length === 0) return null;
209
+ return (
210
+ <div>
211
+ <div class="text-text-muted mb-1.5 font-medium">Attributes:</div>
212
+ <div class="space-y-1.5">
213
+ {filteredAttrs.map(([k, v]) => {
214
+ const isComplex = typeof v === "object" || (typeof v === "string" && (v.includes("\n") || (v.startsWith("{") || v.startsWith("[")) && v.length > 2));
215
+ return (
216
+ <div key={k} class="group">
217
+ <div class="flex items-center gap-1.5">
218
+ <span class="text-text-secondary font-mono text-[11px]">{k}</span>
219
+ {onAddAttributeFilter && (
220
+ <span class="invisible group-hover:visible flex gap-0.5">
221
+ <button
222
+ onClick={(e) => { e.stopPropagation(); onAddAttributeFilter(k, String(v), "include"); }}
223
+ class="text-emerald-500 hover:text-emerald-700 text-[10px] leading-none"
224
+ title="Include filter"
225
+ >+</button>
226
+ <button
227
+ onClick={(e) => { e.stopPropagation(); onAddAttributeFilter(k, String(v), "exclude"); }}
228
+ class="text-red-500 hover:text-red-700 text-[10px] leading-none"
229
+ title="Exclude filter"
230
+ >{"\u2212"}</button>
231
+ </span>
232
+ )}
233
+ {!isComplex && (
234
+ <span class="font-mono text-[11px] ml-auto"><AttributeValue value={v} /></span>
235
+ )}
236
+ </div>
237
+ {isComplex && (
238
+ <div class="mt-0.5 font-mono text-[11px]"><AttributeValue value={v} /></div>
239
+ )}
240
+ </div>
241
+ );
242
+ })}
243
+ </div>
244
+ </div>
245
+ );
246
+ })()}
247
+ {/* Events */}
248
+ {spanEvents.length > 0 && (
249
+ <div>
250
+ <div class="text-text-muted mb-1">Events:</div>
251
+ {spanEvents.map(ev => (
252
+ <div key={ev.id} class={`py-1 px-2 rounded-md mb-1 ${ev.name !== "exception" ? "bg-panel border border-border-subtle" : ""}`} style={ev.name === "exception" ? { background: "var(--color-error-highlight)" } : undefined}>
253
+ <div class="flex items-center gap-2">
254
+ <span class="font-medium">{ev.name}</span>
255
+ {ev.level && <EventLevelBadge level={ev.level} />}
256
+ <span class="text-text-muted font-mono ml-auto">
257
+ +{Math.round(ev.timestamp - span.startTime)}ms
258
+ </span>
259
+ </div>
260
+ {ev.message && <div class="text-text-data mt-0.5 font-mono break-all">{ev.message}</div>}
261
+ </div>
262
+ ))}
263
+ </div>
264
+ )}
265
+ </div>
266
+ </div>
267
+ )}
268
+ </div>
269
+ );
270
+ })}
271
+ </div>
272
+ </div>
273
+ );
274
+ }
275
+
276
+ // ─── Smart Attribute Rendering ───────────────────────────────────────
277
+
278
+ const URL_REGEX = /^https?:\/\/[^\s]+$/;
279
+
280
+ function CollapsibleBlock({ content, lines }: { content: string; lines: number }) {
281
+ const [expanded, setExpanded] = useState(false);
282
+ const isLong = lines > 8;
283
+
284
+ return (
285
+ <div class="relative">
286
+ <pre
287
+ class={`text-ink bg-panel border border-border p-2 rounded-md overflow-x-auto text-[11px] whitespace-pre-wrap break-all ${
288
+ isLong && !expanded ? "max-h-[160px] overflow-hidden" : ""
289
+ }`}
290
+ >{content}</pre>
291
+ {isLong && (
292
+ <button
293
+ onClick={(e) => { e.stopPropagation(); setExpanded(!expanded); }}
294
+ class={`text-[10px] text-blue-500 hover:text-blue-700 font-medium mt-0.5 ${
295
+ !expanded ? "absolute bottom-0 left-0 right-0 pt-6 pb-1 text-center bg-gradient-to-t from-panel via-panel/90 to-transparent rounded-b-md" : ""
296
+ }`}
297
+ >
298
+ {expanded ? "Show less" : `Show all (${lines} lines)`}
299
+ </button>
300
+ )}
301
+ </div>
302
+ );
303
+ }
304
+
305
+ export function AttributeValue({ value }: { value: unknown }) {
306
+ if (value === null || value === undefined) {
307
+ return <span class="text-text-muted italic">null</span>;
308
+ }
309
+ if (typeof value === "boolean") {
310
+ return <span class="text-orange-600">{String(value)}</span>;
311
+ }
312
+ if (typeof value === "number") {
313
+ return <span class="text-purple-600">{value}</span>;
314
+ }
315
+ if (typeof value === "object") {
316
+ const formatted = JSON.stringify(value, null, 2);
317
+ const lines = formatted.split("\n").length;
318
+ return <CollapsibleBlock content={formatted} lines={lines} />;
319
+ }
320
+ const str = String(value);
321
+ if (URL_REGEX.test(str)) {
322
+ return <a href={str} target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:underline break-all">{str}</a>;
323
+ }
324
+ // Try JSON
325
+ if ((str.startsWith("{") || str.startsWith("[")) && str.length > 2) {
326
+ try {
327
+ const parsed = JSON.parse(str);
328
+ const formatted = JSON.stringify(parsed, null, 2);
329
+ const lines = formatted.split("\n").length;
330
+ return <CollapsibleBlock content={formatted} lines={lines} />;
331
+ } catch {}
332
+ }
333
+ // Multiline
334
+ if (str.includes("\n")) {
335
+ const lines = str.split("\n").length;
336
+ return <CollapsibleBlock content={str} lines={lines} />;
337
+ }
338
+ return <span class="text-ink">{str}</span>;
339
+ }
340
+
341
+ // ─── Shared Helpers ──────────────────────────────────────────────────
342
+
343
+ export function EventLevelBadge({ level }: { level: string }) {
344
+ const upper = level.toUpperCase();
345
+ const styles: Record<string, { bg: string; color: string } | null> = {
346
+ ERROR: { bg: "var(--color-badge-red-bg)", color: "var(--color-badge-red-text)" },
347
+ WARN: { bg: "var(--color-badge-orange-bg)", color: "var(--color-badge-orange-text)" },
348
+ WARNING: { bg: "var(--color-badge-orange-bg)", color: "var(--color-badge-orange-text)" },
349
+ INFO: { bg: "var(--color-badge-blue-bg)", color: "var(--color-badge-blue-text)" },
350
+ LOG: null,
351
+ DEBUG: null,
352
+ };
353
+ const s = styles[upper];
354
+ return (
355
+ <span
356
+ class={`inline-flex px-1.5 py-0.5 rounded-md text-[10px] font-medium ${!s ? "bg-panel-secondary text-text-secondary" : ""}`}
357
+ style={s ? { background: s.bg, color: s.color } : undefined}
358
+ >
359
+ {upper}
360
+ </span>
361
+ );
362
+ }
363
+
364
+ export function TraceStatusBadge({ status }: { status: string }) {
365
+ const styles: Record<string, { bg: string; color: string } | null> = {
366
+ ok: { bg: "var(--color-badge-emerald-bg)", color: "var(--color-badge-emerald-text)" },
367
+ error: { bg: "var(--color-badge-red-bg)", color: "var(--color-badge-red-text)" },
368
+ unset: null,
369
+ };
370
+ const s = styles[status];
371
+ return (
372
+ <span
373
+ class={`inline-flex px-2 py-0.5 rounded-md text-xs font-medium ${!s ? "bg-panel-hover text-text-secondary" : ""} ${status === "unset" ? "animate-pulse" : ""}`}
374
+ style={s ? { background: s.bg, color: s.color } : undefined}
375
+ >
376
+ {status === "unset" ? "running" : status}
377
+ </span>
378
+ );
379
+ }
380
+
381
+ export function getKeyAttributes(attrs: Record<string, unknown>, max: number): [string, unknown][] {
382
+ const priorityKeys = ["http.method", "http.status_code", "http.url", "http.route", "db.system", "db.operation", "rpc.method"];
383
+ const entries = Object.entries(attrs);
384
+ const result: [string, unknown][] = [];
385
+ for (const key of priorityKeys) {
386
+ if (result.length >= max) break;
387
+ const entry = entries.find(([k]) => k === key);
388
+ if (entry) result.push(entry);
389
+ }
390
+ if (result.length < max) {
391
+ for (const entry of entries) {
392
+ if (result.length >= max) break;
393
+ if (!result.some(([k]) => k === entry[0]) && typeof entry[1] !== "object") {
394
+ result.push(entry);
395
+ }
396
+ }
397
+ }
398
+ return result;
399
+ }
400
+
401
+ export function formatDuration(ms: number): string {
402
+ if (ms < 1) return "<1ms";
403
+ if (ms < 1000) return `${Math.round(ms)}ms`;
404
+ return `${(ms / 1000).toFixed(2)}s`;
405
+ }
406
+
407
+ export function formatTimestamp(ts: number): string {
408
+ const d = new Date(ts);
409
+ 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")}`;
410
+ }