lopata 0.5.2 → 0.7.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.
@@ -5044,28 +5044,38 @@ function TraceWaterfall({ spans, events, highlightSpanId, onAddAttributeFilter }
5044
5044
  childMap.set(key, []);
5045
5045
  childMap.get(key).push(s3);
5046
5046
  }
5047
+ const processedSpanIdsRef = A2(new Set);
5047
5048
  const spanIdKey = spans.map((s3) => s3.spanId).join(",");
5048
5049
  y2(() => {
5049
5050
  if (spans.length === 0)
5050
5051
  return;
5051
- const autoCollapsed = new Set;
5052
- function walk(parentId, depth) {
5053
- const children = childMap.get(parentId) ?? [];
5054
- for (const child of children) {
5055
- const hasChildren = (childMap.get(child.spanId) ?? []).length > 0;
5056
- if (hasChildren) {
5057
- const spanDur = child.durationMs ?? 0;
5058
- const significantDuration = spanDur > traceDuration * 0.1;
5059
- if (depth >= 2 && !significantDuration) {
5060
- autoCollapsed.add(child.spanId);
5052
+ const previousProcessed = processedSpanIdsRef.current;
5053
+ const currentIds = spans.map((s3) => s3.spanId);
5054
+ const newSpanIds = new Set(currentIds.filter((id) => !previousProcessed.has(id)));
5055
+ processedSpanIdsRef.current = new Set(currentIds);
5056
+ if (newSpanIds.size === 0)
5057
+ return;
5058
+ setCollapsedSpans((prev) => {
5059
+ const next = new Set(prev);
5060
+ function walk(parentId, depth) {
5061
+ const children = childMap.get(parentId) ?? [];
5062
+ for (const child of children) {
5063
+ if (newSpanIds.has(child.spanId)) {
5064
+ const hasChildren = (childMap.get(child.spanId) ?? []).length > 0;
5065
+ if (hasChildren) {
5066
+ const spanDur = child.durationMs ?? 0;
5067
+ const significantDuration = spanDur > traceDuration * 0.1;
5068
+ if (depth >= 2 && !significantDuration) {
5069
+ next.add(child.spanId);
5070
+ }
5071
+ }
5061
5072
  }
5073
+ walk(child.spanId, depth + 1);
5062
5074
  }
5063
- walk(child.spanId, depth + 1);
5064
5075
  }
5065
- }
5066
- walk(null, 0);
5067
- setCollapsedSpans(autoCollapsed);
5068
- setExpandedSpan(null);
5076
+ walk(null, 0);
5077
+ return next;
5078
+ });
5069
5079
  }, [spanIdKey]);
5070
5080
  const toggleCollapse = (spanId) => {
5071
5081
  setCollapsedSpans((prev) => {
@@ -5090,6 +5100,19 @@ function TraceWaterfall({ spans, events, highlightSpanId, onAddAttributeFilter }
5090
5100
  return result;
5091
5101
  }
5092
5102
  const flatSpans = flattenTree(null, 0);
5103
+ const descendantErrorCount = new Map;
5104
+ function countErrors(parentId) {
5105
+ let count = 0;
5106
+ for (const child of childMap.get(parentId) ?? []) {
5107
+ if (child.status === "error")
5108
+ count++;
5109
+ const childErrors = countErrors(child.spanId);
5110
+ count += childErrors;
5111
+ descendantErrorCount.set(child.spanId, childErrors);
5112
+ }
5113
+ return count;
5114
+ }
5115
+ countErrors(null);
5093
5116
  const getParentAttributes = (span) => {
5094
5117
  if (!span.parentSpanId)
5095
5118
  return {};
@@ -5146,8 +5169,13 @@ function TraceWaterfall({ spans, events, highlightSpanId, onAddAttributeFilter }
5146
5169
  class: "inline-block w-4 flex-shrink-0"
5147
5170
  }, undefined, false, undefined, this),
5148
5171
  /* @__PURE__ */ u3("span", {
5149
- class: "truncate",
5172
+ class: `truncate ${span.status === "error" ? "text-red-400" : ""}`,
5150
5173
  children: span.name
5174
+ }, undefined, false, undefined, this),
5175
+ (descendantErrorCount.get(span.spanId) ?? 0) > 0 && /* @__PURE__ */ u3("span", {
5176
+ class: "ml-1 flex-shrink-0 inline-flex items-center justify-center min-w-[16px] h-4 px-1 rounded-full text-[10px] font-medium",
5177
+ style: { background: "var(--color-badge-red-bg)", color: "var(--color-badge-red-text)" },
5178
+ children: descendantErrorCount.get(span.spanId)
5151
5179
  }, undefined, false, undefined, this)
5152
5180
  ]
5153
5181
  }, undefined, true, undefined, this),
@@ -5718,14 +5746,11 @@ function ErrorList() {
5718
5746
  /* @__PURE__ */ u3("td", {
5719
5747
  class: "px-4 py-2.5 text-right",
5720
5748
  children: err.traceId ? /* @__PURE__ */ u3("a", {
5721
- href: `#/traces?trace=${err.traceId}`,
5749
+ href: `#/traces/${err.traceId}`,
5722
5750
  onClick: (e3) => e3.stopPropagation(),
5723
5751
  class: "text-link hover:text-accent-lime text-xs font-mono",
5724
- children: [
5725
- err.traceId.slice(0, 8),
5726
- "..."
5727
- ]
5728
- }, undefined, true, undefined, this) : /* @__PURE__ */ u3("span", {
5752
+ children: err.traceId
5753
+ }, undefined, false, undefined, this) : /* @__PURE__ */ u3("span", {
5729
5754
  class: "text-text-dim text-xs",
5730
5755
  children: "-"
5731
5756
  }, undefined, false, undefined, this)
@@ -5883,14 +5908,30 @@ function ErrorDetailPage({ errorId }) {
5883
5908
  }, undefined, false, undefined, this)
5884
5909
  }, undefined, false, undefined, this)
5885
5910
  }, undefined, false, undefined, this),
5886
- traceSpans && traceSpans.length > 0 && /* @__PURE__ */ u3(Section, {
5911
+ detail.traceId && /* @__PURE__ */ u3(Section, {
5887
5912
  title: "Trace",
5888
5913
  open: true,
5889
- children: /* @__PURE__ */ u3(SimpleTraceWaterfall, {
5890
- spans: traceSpans,
5891
- highlightSpanId: detail.spanId
5892
- }, undefined, false, undefined, this)
5893
- }, undefined, false, undefined, this),
5914
+ children: [
5915
+ /* @__PURE__ */ u3("div", {
5916
+ class: "px-4 py-2.5 border-b border-border-subtle flex items-center gap-2",
5917
+ children: [
5918
+ /* @__PURE__ */ u3("span", {
5919
+ class: "text-xs text-text-muted",
5920
+ children: "Trace ID:"
5921
+ }, undefined, false, undefined, this),
5922
+ /* @__PURE__ */ u3("a", {
5923
+ href: `#/traces/${detail.traceId}`,
5924
+ class: "text-link hover:text-accent-lime text-xs font-mono",
5925
+ children: detail.traceId
5926
+ }, undefined, false, undefined, this)
5927
+ ]
5928
+ }, undefined, true, undefined, this),
5929
+ traceSpans && traceSpans.length > 0 && /* @__PURE__ */ u3(SimpleTraceWaterfall, {
5930
+ spans: traceSpans,
5931
+ highlightSpanId: detail.spanId
5932
+ }, undefined, false, undefined, this)
5933
+ ]
5934
+ }, undefined, true, undefined, this),
5894
5935
  data.request.method && data.request.url && /* @__PURE__ */ u3(Section, {
5895
5936
  title: "Request",
5896
5937
  open: true,
@@ -7554,6 +7595,14 @@ function ScheduledList() {
7554
7595
  }
7555
7596
 
7556
7597
  // src/dashboard/views/traces.tsx
7598
+ var SOURCE_BADGE_STYLES = {
7599
+ fetch: { bg: "var(--color-badge-blue-bg)", color: "var(--color-badge-blue-text)" },
7600
+ scheduled: { bg: "var(--color-badge-purple-bg)", color: "var(--color-badge-purple-text)" },
7601
+ queue: { bg: "var(--color-badge-orange-bg)", color: "var(--color-badge-orange-text)" },
7602
+ alarm: { bg: "var(--color-badge-yellow-bg)", color: "var(--color-badge-yellow-text)" },
7603
+ workflow: { bg: "var(--color-badge-emerald-bg)", color: "var(--color-badge-emerald-text)" }
7604
+ };
7605
+ var DEFAULT_BADGE_STYLE = { bg: "var(--color-badge-red-bg)", color: "var(--color-badge-red-text)" };
7557
7606
  var eventListeners = new Set;
7558
7607
  function onTraceEvents(fn) {
7559
7608
  eventListeners.add(fn);
@@ -7568,6 +7617,39 @@ function emitTraceEvents(events) {
7568
7617
  } catch {}
7569
7618
  }
7570
7619
  }
7620
+ function useTraceData(traceId) {
7621
+ const [spans, setSpans] = d2([]);
7622
+ const [events, setEvents] = d2([]);
7623
+ const [traceErrors, setTraceErrors] = d2([]);
7624
+ const [isLoading, setIsLoading] = d2(true);
7625
+ y2(() => {
7626
+ setIsLoading(true);
7627
+ rpc("traces.getTrace", { traceId }).then((data) => {
7628
+ setSpans(data.spans);
7629
+ setEvents(data.events);
7630
+ setIsLoading(false);
7631
+ });
7632
+ rpc("traces.errors", { traceId }).then(setTraceErrors).catch(() => {});
7633
+ }, [traceId]);
7634
+ y2(() => {
7635
+ return onTraceEvents((traceEvents) => {
7636
+ for (const ev of traceEvents) {
7637
+ if (ev.type === "span.start" && ev.span.traceId === traceId) {
7638
+ setSpans((prev) => {
7639
+ if (prev.some((s3) => s3.spanId === ev.span.spanId))
7640
+ return prev;
7641
+ return [...prev, ev.span];
7642
+ });
7643
+ } else if (ev.type === "span.end" && ev.span.traceId === traceId) {
7644
+ setSpans((prev) => prev.map((s3) => s3.spanId === ev.span.spanId ? ev.span : s3));
7645
+ } else if (ev.type === "span.event" && ev.event.traceId === traceId) {
7646
+ setEvents((prev) => [...prev, ev.event]);
7647
+ }
7648
+ }
7649
+ });
7650
+ }, [traceId]);
7651
+ return { spans, events, traceErrors, isLoading };
7652
+ }
7571
7653
  function useTraceStream() {
7572
7654
  const [traces, setTraces] = d2(new Map);
7573
7655
  const [wsStatus, setWsStatus] = d2("connecting");
@@ -7679,7 +7761,16 @@ var TIME_RANGE_OPTIONS = [
7679
7761
  { label: "24h", ms: 24 * 60 * 60 * 1000 },
7680
7762
  { label: "All", ms: 0 }
7681
7763
  ];
7682
- function TracesView() {
7764
+ function TracesView({ route }) {
7765
+ const traceIdFromRoute = route.startsWith("/traces/") ? route.slice("/traces/".length) : null;
7766
+ if (traceIdFromRoute) {
7767
+ return /* @__PURE__ */ u3(TraceDetailPage, {
7768
+ traceId: traceIdFromRoute
7769
+ }, undefined, false, undefined, this);
7770
+ }
7771
+ return /* @__PURE__ */ u3(TracesListView, {}, undefined, false, undefined, this);
7772
+ }
7773
+ function TracesListView() {
7683
7774
  const { traces, setFilter, wsStatus } = useTraceStream();
7684
7775
  const [selectedTraceId, setSelectedTraceId] = d2(null);
7685
7776
  const [pathFilter, setPathFilter] = d2("");
@@ -7766,10 +7857,7 @@ function TracesView() {
7766
7857
  ]
7767
7858
  }, undefined, true, undefined, this),
7768
7859
  /* @__PURE__ */ u3("button", {
7769
- onClick: () => {
7770
- clearTraces.mutate();
7771
- setSelectedTraceId(null);
7772
- },
7860
+ onClick: () => clearTraces.mutate(),
7773
7861
  class: "rounded-md px-3 py-1.5 text-sm font-medium bg-panel border border-border text-text-secondary btn-danger transition-all",
7774
7862
  children: "Clear all"
7775
7863
  }, undefined, false, undefined, this)
@@ -7973,6 +8061,132 @@ function TracesView() {
7973
8061
  ]
7974
8062
  }, undefined, true, undefined, this);
7975
8063
  }
8064
+ function TraceDetailPage({ traceId }) {
8065
+ const { spans, events, traceErrors, isLoading } = useTraceData(traceId);
8066
+ const rootSpan = spans.find((s3) => !s3.parentSpanId);
8067
+ return /* @__PURE__ */ u3("div", {
8068
+ class: "p-4 sm:p-8 h-full flex flex-col",
8069
+ children: [
8070
+ /* @__PURE__ */ u3("div", {
8071
+ class: "flex items-center gap-3 mb-6",
8072
+ children: [
8073
+ /* @__PURE__ */ u3("button", {
8074
+ onClick: () => navigate("/traces"),
8075
+ class: "flex items-center gap-1.5 text-sm text-text-secondary hover:text-ink transition-colors",
8076
+ children: [
8077
+ /* @__PURE__ */ u3("svg", {
8078
+ class: "w-4 h-4",
8079
+ viewBox: "0 0 20 20",
8080
+ fill: "currentColor",
8081
+ children: /* @__PURE__ */ u3("path", {
8082
+ "fill-rule": "evenodd",
8083
+ d: "M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z",
8084
+ "clip-rule": "evenodd"
8085
+ }, undefined, false, undefined, this)
8086
+ }, undefined, false, undefined, this),
8087
+ "Back to traces"
8088
+ ]
8089
+ }, undefined, true, undefined, this),
8090
+ /* @__PURE__ */ u3("div", {
8091
+ class: "h-5 w-px bg-border"
8092
+ }, undefined, false, undefined, this),
8093
+ /* @__PURE__ */ u3("div", {
8094
+ children: [
8095
+ /* @__PURE__ */ u3("div", {
8096
+ class: "flex items-center gap-2",
8097
+ children: [
8098
+ rootSpan && /* @__PURE__ */ u3(TraceStatusBadge, {
8099
+ status: rootSpan.status
8100
+ }, undefined, false, undefined, this),
8101
+ /* @__PURE__ */ u3("h1", {
8102
+ class: "text-lg font-bold text-ink",
8103
+ children: rootSpan?.name ?? "Loading..."
8104
+ }, undefined, false, undefined, this)
8105
+ ]
8106
+ }, undefined, true, undefined, this),
8107
+ /* @__PURE__ */ u3("div", {
8108
+ class: "text-xs text-text-muted font-mono mt-0.5",
8109
+ children: [
8110
+ "Trace ",
8111
+ traceId,
8112
+ rootSpan?.workerName && /* @__PURE__ */ u3("span", {
8113
+ class: "ml-2 inline-flex px-2 py-0.5 rounded-md text-xs font-medium bg-panel-hover text-text-secondary",
8114
+ children: rootSpan.workerName
8115
+ }, undefined, false, undefined, this),
8116
+ rootSpan?.durationMs != null && /* @__PURE__ */ u3("span", {
8117
+ class: "ml-2 text-text-secondary",
8118
+ children: formatDuration(rootSpan.durationMs)
8119
+ }, undefined, false, undefined, this)
8120
+ ]
8121
+ }, undefined, true, undefined, this)
8122
+ ]
8123
+ }, undefined, true, undefined, this)
8124
+ ]
8125
+ }, undefined, true, undefined, this),
8126
+ /* @__PURE__ */ u3("div", {
8127
+ class: "flex-1 overflow-y-auto scrollbar-thin min-h-0",
8128
+ children: isLoading ? /* @__PURE__ */ u3("div", {
8129
+ class: "text-text-muted text-sm py-12 text-center",
8130
+ children: "Loading trace..."
8131
+ }, undefined, false, undefined, this) : /* @__PURE__ */ u3("div", {
8132
+ children: [
8133
+ traceErrors.length > 0 && /* @__PURE__ */ u3("div", {
8134
+ class: "mb-4",
8135
+ children: [
8136
+ /* @__PURE__ */ u3("div", {
8137
+ class: "text-xs font-medium text-text-muted uppercase tracking-wider mb-2",
8138
+ children: [
8139
+ "Errors (",
8140
+ traceErrors.length,
8141
+ ")"
8142
+ ]
8143
+ }, undefined, true, undefined, this),
8144
+ /* @__PURE__ */ u3("div", {
8145
+ class: "space-y-1",
8146
+ children: traceErrors.map((err) => /* @__PURE__ */ u3("a", {
8147
+ href: `#/errors/${err.id}`,
8148
+ class: "flex items-center gap-2 px-3 py-2 rounded-md text-xs no-underline transition-colors",
8149
+ style: { background: "var(--color-error-highlight)", borderColor: "var(--color-error-ring)" },
8150
+ children: [
8151
+ err.source && (() => {
8152
+ const s3 = SOURCE_BADGE_STYLES[err.source] ?? DEFAULT_BADGE_STYLE;
8153
+ return /* @__PURE__ */ u3("span", {
8154
+ class: "inline-flex px-1.5 py-0.5 rounded text-[10px] font-medium",
8155
+ style: { background: s3.bg, color: s3.color },
8156
+ children: err.source
8157
+ }, undefined, false, undefined, this);
8158
+ })(),
8159
+ /* @__PURE__ */ u3("span", {
8160
+ class: "font-medium",
8161
+ style: { color: "var(--color-badge-red-text)" },
8162
+ children: err.errorName
8163
+ }, undefined, false, undefined, this),
8164
+ /* @__PURE__ */ u3("span", {
8165
+ style: { color: "var(--color-badge-red-text)" },
8166
+ class: "truncate",
8167
+ children: err.errorMessage
8168
+ }, undefined, false, undefined, this),
8169
+ /* @__PURE__ */ u3("span", {
8170
+ style: { color: "var(--color-badge-red-text)", opacity: 0.7 },
8171
+ class: "font-mono ml-auto flex-shrink-0",
8172
+ children: formatTimestamp(err.timestamp)
8173
+ }, undefined, false, undefined, this)
8174
+ ]
8175
+ }, err.id, true, undefined, this))
8176
+ }, undefined, false, undefined, this)
8177
+ ]
8178
+ }, undefined, true, undefined, this),
8179
+ /* @__PURE__ */ u3(TraceWaterfall, {
8180
+ spans,
8181
+ events,
8182
+ onAddAttributeFilter: () => {}
8183
+ }, undefined, false, undefined, this)
8184
+ ]
8185
+ }, undefined, true, undefined, this)
8186
+ }, undefined, false, undefined, this)
8187
+ ]
8188
+ }, undefined, true, undefined, this);
8189
+ }
7976
8190
  function SpansListTab() {
7977
8191
  const [spans, setSpans] = d2([]);
7978
8192
  const [cursor, setCursor] = d2(null);
@@ -8070,11 +8284,8 @@ function SpansListTab() {
8070
8284
  children: /* @__PURE__ */ u3("button", {
8071
8285
  onClick: () => setSelectedTraceId(span.traceId),
8072
8286
  class: "text-link hover:text-accent-lime text-xs font-mono",
8073
- children: [
8074
- span.traceId.slice(0, 8),
8075
- "..."
8076
- ]
8077
- }, undefined, true, undefined, this)
8287
+ children: span.traceId
8288
+ }, undefined, false, undefined, this)
8078
8289
  }, undefined, false, undefined, this)
8079
8290
  ]
8080
8291
  }, span.spanId, true, undefined, this))
@@ -8098,8 +8309,7 @@ function SpansListTab() {
8098
8309
  }, undefined, false, undefined, this),
8099
8310
  selectedTraceId && /* @__PURE__ */ u3(TraceDrawer, {
8100
8311
  traceId: selectedTraceId,
8101
- onClose: () => setSelectedTraceId(null),
8102
- onAddAttributeFilter: () => {}
8312
+ onClose: () => setSelectedTraceId(null)
8103
8313
  }, undefined, false, undefined, this)
8104
8314
  ]
8105
8315
  }, undefined, true, undefined, this);
@@ -8108,6 +8318,7 @@ function LogsListTab() {
8108
8318
  const [logs, setLogs] = d2([]);
8109
8319
  const [cursor, setCursor] = d2(null);
8110
8320
  const [isLoading, setIsLoading] = d2(true);
8321
+ const [selectedTraceId, setSelectedTraceId] = d2(null);
8111
8322
  const loadLogs = q2((cur) => {
8112
8323
  setIsLoading(true);
8113
8324
  rpc("traces.listLogs", { limit: 50, cursor: cur }).then((data) => {
@@ -8188,12 +8399,13 @@ function LogsListTab() {
8188
8399
  children: formatTimestamp(log.timestamp)
8189
8400
  }, undefined, false, undefined, this),
8190
8401
  /* @__PURE__ */ u3("td", {
8191
- class: "px-4 py-2.5 text-right font-mono text-xs text-text-muted",
8192
- children: [
8193
- log.traceId.slice(0, 8),
8194
- "..."
8195
- ]
8196
- }, undefined, true, undefined, this)
8402
+ class: "px-4 py-2.5 text-right",
8403
+ children: /* @__PURE__ */ u3("button", {
8404
+ onClick: () => setSelectedTraceId(log.traceId),
8405
+ class: "text-link hover:text-accent-lime text-xs font-mono",
8406
+ children: log.traceId
8407
+ }, undefined, false, undefined, this)
8408
+ }, undefined, false, undefined, this)
8197
8409
  ]
8198
8410
  }, log.id, true, undefined, this))
8199
8411
  }, undefined, false, undefined, this)
@@ -8213,49 +8425,16 @@ function LogsListTab() {
8213
8425
  isLoading && logs.length === 0 && /* @__PURE__ */ u3("div", {
8214
8426
  class: "text-text-muted text-sm text-center py-12",
8215
8427
  children: "Loading logs..."
8428
+ }, undefined, false, undefined, this),
8429
+ selectedTraceId && /* @__PURE__ */ u3(TraceDrawer, {
8430
+ traceId: selectedTraceId,
8431
+ onClose: () => setSelectedTraceId(null)
8216
8432
  }, undefined, false, undefined, this)
8217
8433
  ]
8218
8434
  }, undefined, true, undefined, this);
8219
8435
  }
8220
- var SOURCE_BADGE_STYLES = {
8221
- fetch: { bg: "var(--color-badge-blue-bg)", color: "var(--color-badge-blue-text)" },
8222
- scheduled: { bg: "var(--color-badge-purple-bg)", color: "var(--color-badge-purple-text)" },
8223
- queue: { bg: "var(--color-badge-orange-bg)", color: "var(--color-badge-orange-text)" },
8224
- alarm: { bg: "var(--color-badge-yellow-bg)", color: "var(--color-badge-yellow-text)" },
8225
- workflow: { bg: "var(--color-badge-emerald-bg)", color: "var(--color-badge-emerald-text)" }
8226
- };
8227
- var DEFAULT_BADGE_STYLE = { bg: "var(--color-badge-red-bg)", color: "var(--color-badge-red-text)" };
8228
8436
  function TraceDrawer({ traceId, onClose, onAddAttributeFilter }) {
8229
- const [spans, setSpans] = d2([]);
8230
- const [events, setEvents] = d2([]);
8231
- const [traceErrors, setTraceErrors] = d2([]);
8232
- const [isLoading, setIsLoading] = d2(true);
8233
- y2(() => {
8234
- setIsLoading(true);
8235
- rpc("traces.getTrace", { traceId }).then((data) => {
8236
- setSpans(data.spans);
8237
- setEvents(data.events);
8238
- setIsLoading(false);
8239
- });
8240
- rpc("traces.errors", { traceId }).then(setTraceErrors).catch(() => {});
8241
- }, [traceId]);
8242
- y2(() => {
8243
- return onTraceEvents((traceEvents) => {
8244
- for (const ev of traceEvents) {
8245
- if (ev.type === "span.start" && ev.span.traceId === traceId) {
8246
- setSpans((prev) => {
8247
- if (prev.some((s3) => s3.spanId === ev.span.spanId))
8248
- return prev;
8249
- return [...prev, ev.span];
8250
- });
8251
- } else if (ev.type === "span.end" && ev.span.traceId === traceId) {
8252
- setSpans((prev) => prev.map((s3) => s3.spanId === ev.span.spanId ? ev.span : s3));
8253
- } else if (ev.type === "span.event" && ev.event.traceId === traceId) {
8254
- setEvents((prev) => [...prev, ev.event]);
8255
- }
8256
- }
8257
- });
8258
- }, [traceId]);
8437
+ const { spans, events, traceErrors, isLoading } = useTraceData(traceId);
8259
8438
  return /* @__PURE__ */ u3(k, {
8260
8439
  children: [
8261
8440
  /* @__PURE__ */ u3("div", {
@@ -8274,8 +8453,7 @@ function TraceDrawer({ traceId, onClose, onAddAttributeFilter }) {
8274
8453
  class: "text-xs text-text-muted font-mono",
8275
8454
  children: [
8276
8455
  "Trace ",
8277
- traceId.slice(0, 12),
8278
- "..."
8456
+ traceId
8279
8457
  ]
8280
8458
  }, undefined, true, undefined, this),
8281
8459
  /* @__PURE__ */ u3("div", {
@@ -8284,11 +8462,34 @@ function TraceDrawer({ traceId, onClose, onAddAttributeFilter }) {
8284
8462
  }, undefined, false, undefined, this)
8285
8463
  ]
8286
8464
  }, undefined, true, undefined, this),
8287
- /* @__PURE__ */ u3("button", {
8288
- onClick: onClose,
8289
- class: "w-7 h-7 flex items-center justify-center rounded-md hover:bg-panel-hover transition-colors text-text-muted hover:text-ink",
8290
- children: "×"
8291
- }, undefined, false, undefined, this)
8465
+ /* @__PURE__ */ u3("div", {
8466
+ class: "flex items-center gap-1",
8467
+ children: [
8468
+ /* @__PURE__ */ u3("a", {
8469
+ href: `#/traces/${traceId}`,
8470
+ class: "w-7 h-7 flex items-center justify-center rounded-md hover:bg-panel-hover transition-colors text-text-muted hover:text-ink",
8471
+ title: "Open full page",
8472
+ children: /* @__PURE__ */ u3("svg", {
8473
+ class: "w-4 h-4",
8474
+ viewBox: "0 0 20 20",
8475
+ fill: "currentColor",
8476
+ children: [
8477
+ /* @__PURE__ */ u3("path", {
8478
+ d: "M4.75 5.75a.75.75 0 00.75.75h4.69l-4.22 4.22a.75.75 0 001.06 1.06L11.25 7.56v4.69a.75.75 0 001.5 0v-6.5a.75.75 0 00-.75-.75h-6.5a.75.75 0 00-.75.75z"
8479
+ }, undefined, false, undefined, this),
8480
+ /* @__PURE__ */ u3("path", {
8481
+ d: "M3 13.5a1.5 1.5 0 011.5-1.5h1.25a.75.75 0 010 1.5H4.5v4h4v-1.25a.75.75 0 011.5 0v1.25a1.5 1.5 0 01-1.5 1.5h-4A1.5 1.5 0 013 17.5v-4z"
8482
+ }, undefined, false, undefined, this)
8483
+ ]
8484
+ }, undefined, true, undefined, this)
8485
+ }, undefined, false, undefined, this),
8486
+ /* @__PURE__ */ u3("button", {
8487
+ onClick: onClose,
8488
+ class: "w-7 h-7 flex items-center justify-center rounded-md hover:bg-panel-hover transition-colors text-text-muted hover:text-ink",
8489
+ children: "×"
8490
+ }, undefined, false, undefined, this)
8491
+ ]
8492
+ }, undefined, true, undefined, this)
8292
8493
  ]
8293
8494
  }, undefined, true, undefined, this),
8294
8495
  /* @__PURE__ */ u3("div", {
@@ -8347,7 +8548,7 @@ function TraceDrawer({ traceId, onClose, onAddAttributeFilter }) {
8347
8548
  /* @__PURE__ */ u3(TraceWaterfall, {
8348
8549
  spans,
8349
8550
  events,
8350
- onAddAttributeFilter
8551
+ onAddAttributeFilter: onAddAttributeFilter ?? (() => {})
8351
8552
  }, undefined, false, undefined, this)
8352
8553
  ]
8353
8554
  }, undefined, true, undefined, this)
@@ -9333,7 +9534,9 @@ function App() {
9333
9534
  route
9334
9535
  }, undefined, false, undefined, this);
9335
9536
  if (route.startsWith("/traces"))
9336
- return /* @__PURE__ */ u3(TracesView, {}, undefined, false, undefined, this);
9537
+ return /* @__PURE__ */ u3(TracesView, {
9538
+ route
9539
+ }, undefined, false, undefined, this);
9337
9540
  if (route.startsWith("/workers"))
9338
9541
  return /* @__PURE__ */ u3(WorkersView, {}, undefined, false, undefined, this);
9339
9542
  if (route.startsWith("/kv"))
@@ -683,6 +683,10 @@
683
683
  height: calc(var(--spacing) * 2.5);
684
684
  }
685
685
 
686
+ .h-4 {
687
+ height: calc(var(--spacing) * 4);
688
+ }
689
+
686
690
  .h-5 {
687
691
  height: calc(var(--spacing) * 5);
688
692
  }
@@ -824,6 +828,10 @@
824
828
  width: 100%;
825
829
  }
826
830
 
831
+ .w-px {
832
+ width: 1px;
833
+ }
834
+
827
835
  .max-w-4xl {
828
836
  max-width: var(--container-4xl);
829
837
  }
@@ -880,6 +888,10 @@
880
888
  min-width: calc(var(--spacing) * 0);
881
889
  }
882
890
 
891
+ .min-w-\[16px\] {
892
+ min-width: 16px;
893
+ }
894
+
883
895
  .min-w-\[20px\] {
884
896
  min-width: 20px;
885
897
  }
@@ -1370,6 +1382,10 @@
1370
1382
  background-color: var(--color-blue-600);
1371
1383
  }
1372
1384
 
1385
+ .bg-border {
1386
+ background-color: var(--color-border);
1387
+ }
1388
+
1373
1389
  .bg-cyan-500\/15 {
1374
1390
  background-color: #00b7d726;
1375
1391
  }
@@ -8,7 +8,7 @@
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
9
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
10
10
 
11
- <link rel="stylesheet" crossorigin href="/__dashboard/assets/chunk-a68x1m5f.css"><script type="module" crossorigin src="/__dashboard/assets/chunk-rae638a4.js"></script></head>
11
+ <link rel="stylesheet" crossorigin href="/__dashboard/assets/chunk-pqnphvm2.css"><script type="module" crossorigin src="/__dashboard/assets/chunk-5nxa3jfc.js"></script></head>
12
12
  <body class="h-full bg-surface text-ink" style="font-family: system-ui, -apple-system, sans-serif;">
13
13
  <script>
14
14
  // Apply saved theme before first paint to prevent flash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lopata",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,6 +28,7 @@ export class BridgeWebSocket extends EventTarget {
28
28
  readonly wsId: string
29
29
  readyState = 1 // OPEN
30
30
  private _postMessage: (msg: WsBridgeOutbound) => void
31
+ private _attachment: any = null
31
32
 
32
33
  constructor(wsId: string, postMessage: (msg: WsBridgeOutbound) => void) {
33
34
  super()
@@ -35,6 +36,14 @@ export class BridgeWebSocket extends EventTarget {
35
36
  this._postMessage = postMessage
36
37
  }
37
38
 
39
+ serializeAttachment(attachment: any): void {
40
+ this._attachment = JSON.parse(JSON.stringify(attachment))
41
+ }
42
+
43
+ deserializeAttachment(): any | null {
44
+ return this._attachment
45
+ }
46
+
38
47
  send(data: string | ArrayBuffer): void {
39
48
  if (this.readyState !== 1) return
40
49
  this._postMessage({ type: 'ws-send', wsId: this.wsId, data })
@@ -602,6 +602,12 @@ export class DurableObjectStateImpl {
602
602
  if (this._acceptedWebSockets.size >= this._limits.maxConcurrentWebSockets) {
603
603
  throw new Error(`Exceeded max concurrent WebSocket connections (${this._limits.maxConcurrentWebSockets})`)
604
604
  }
605
+
606
+ // Implicitly accept the WebSocket (in CF production, ctx.acceptWebSocket handles this)
607
+ if ('accept' in ws && typeof ws.accept === 'function') {
608
+ ;(ws as any).accept()
609
+ }
610
+
605
611
  const entry: AcceptedWebSocket = { ws, tags: tagList, autoResponseTimestamp: null }
606
612
  this._acceptedWebSockets.add(entry)
607
613
 
@@ -41,6 +41,7 @@ export class CFWebSocket extends EventTarget {
41
41
  /** @internal */ _peer: CFWebSocket | null = null
42
42
  /** @internal */ _accepted = false
43
43
  /** @internal */ _eventQueue: WSEvent[] = []
44
+ /** @internal */ _attachment: any = null
44
45
 
45
46
  // Callback-style handlers (standard WebSocket compat)
46
47
  onopen: ((ev: Event) => void) | null = null
@@ -48,6 +49,18 @@ export class CFWebSocket extends EventTarget {
48
49
  onclose: ((ev: CloseEvent) => void) | null = null
49
50
  onerror: ((ev: Event) => void) | null = null
50
51
 
52
+ /**
53
+ * CF-specific: attach serializable data to this WebSocket.
54
+ * Survives hibernation in production; here it's just in-memory.
55
+ */
56
+ serializeAttachment(attachment: any): void {
57
+ this._attachment = JSON.parse(JSON.stringify(attachment))
58
+ }
59
+
60
+ deserializeAttachment(): any | null {
61
+ return this._attachment
62
+ }
63
+
51
64
  /**
52
65
  * CF-specific: begin dispatching events.
53
66
  * Must be called before messages can be sent or received.
package/src/cli/dev.ts CHANGED
@@ -333,6 +333,9 @@ export async function run(ctx: CliContext) {
333
333
  const ce = ev as CloseEvent
334
334
  ws.close(ce.code, ce.reason)
335
335
  })
336
+ // Accept the client side so events from server.send() are dispatched
337
+ // (in CF production the runtime handles this; here we bridge manually)
338
+ cfSocket.accept()
336
339
  },
337
340
  message(ws, message) {
338
341
  const data = ws.data as unknown as Record<string, unknown>