@nwire/studio 0.12.1 → 0.13.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 (130) hide show
  1. package/package.json +5 -3
  2. package/src/App.vue +62 -56
  3. package/src/components/BcCard.stories.ts +47 -0
  4. package/src/components/BcCard.vue +152 -0
  5. package/src/components/DurationBar.stories.ts +55 -0
  6. package/src/components/DurationBar.vue +72 -0
  7. package/src/components/ErrorCard.stories.ts +133 -0
  8. package/src/components/ErrorCard.vue +153 -0
  9. package/src/components/GraphCanvas.stories.ts +48 -0
  10. package/src/components/GraphCanvas.vue +88 -0
  11. package/src/components/KpiTile.stories.ts +32 -0
  12. package/src/components/KpiTile.vue +39 -0
  13. package/src/components/LiveTable.stories.ts +78 -0
  14. package/src/components/LiveTable.vue +186 -0
  15. package/src/components/MetadataInspector.stories.ts +53 -0
  16. package/src/components/MetadataInspector.vue +105 -0
  17. package/src/components/NodeCard.stories.ts +44 -0
  18. package/src/components/NodeCard.vue +150 -0
  19. package/src/components/RcaPanel.stories.ts +95 -0
  20. package/src/components/RcaPanel.vue +223 -0
  21. package/src/components/ServiceNode.vue +134 -0
  22. package/src/components/SourceDrawer.vue +6 -4
  23. package/src/components/SourcePill.vue +10 -3
  24. package/src/components/StatusBadge.stories.ts +33 -0
  25. package/src/components/StatusBadge.vue +54 -0
  26. package/src/components/Waterfall.stories.ts +85 -0
  27. package/src/components/Waterfall.vue +53 -0
  28. package/src/components/WaterfallRow.vue +74 -0
  29. package/src/components/__tests__/BcCard.test.ts +53 -0
  30. package/src/components/__tests__/DurationBar.test.ts +31 -0
  31. package/src/components/__tests__/ErrorCard.test.ts +71 -0
  32. package/src/components/__tests__/KpiTile.test.ts +23 -0
  33. package/src/components/__tests__/LiveTable.test.ts +100 -0
  34. package/src/components/__tests__/MetadataInspector.test.ts +38 -0
  35. package/src/components/__tests__/NodeCard.test.ts +116 -0
  36. package/src/components/__tests__/RcaPanel.test.ts +81 -0
  37. package/src/components/__tests__/StatusBadge.test.ts +23 -0
  38. package/src/components/__tests__/Waterfall.test.ts +54 -0
  39. package/src/components/index.ts +13 -0
  40. package/src/composables/__tests__/composables-context.test.ts +107 -0
  41. package/src/composables/__tests__/useTelemetry.test.ts +104 -0
  42. package/src/composables/__tests__/useTelemetryRuns.test.ts +282 -0
  43. package/src/composables/useDiscovery.ts +73 -0
  44. package/src/composables/useEndpoints.ts +94 -0
  45. package/src/composables/useLogTail.ts +51 -0
  46. package/src/composables/useManifest.ts +43 -0
  47. package/src/composables/useProcesses.ts +114 -0
  48. package/src/composables/useProject.ts +34 -0
  49. package/src/composables/useTelemetry.ts +270 -0
  50. package/src/lib/__tests__/bc-graph.test.ts +218 -0
  51. package/src/lib/__tests__/dispatch-form.test.ts +113 -0
  52. package/src/lib/__tests__/error-friendly.test.ts +198 -0
  53. package/src/lib/__tests__/home.test.ts +231 -0
  54. package/src/lib/__tests__/inspect.test.ts +160 -0
  55. package/src/lib/__tests__/kind-colors.test.ts +59 -0
  56. package/src/lib/__tests__/live-table.test.ts +194 -0
  57. package/src/lib/__tests__/manifest-health.test.ts +120 -0
  58. package/src/lib/__tests__/manifest.test.ts +87 -0
  59. package/src/lib/__tests__/metadata.test.ts +47 -0
  60. package/src/lib/__tests__/node-metrics.test.ts +144 -0
  61. package/src/lib/__tests__/operate.test.ts +97 -0
  62. package/src/lib/__tests__/pipeline-flow.test.ts +79 -0
  63. package/src/lib/__tests__/rca.test.ts +124 -0
  64. package/src/lib/__tests__/telemetry.test.ts +91 -0
  65. package/src/lib/__tests__/topology-graph.test.ts +331 -0
  66. package/src/lib/__tests__/topology-view.test.ts +154 -0
  67. package/src/lib/__tests__/waterfall.test.ts +165 -0
  68. package/src/lib/bc-graph.ts +298 -0
  69. package/src/lib/dispatch-form.ts +160 -0
  70. package/src/lib/error-friendly.ts +288 -0
  71. package/src/lib/home.ts +191 -0
  72. package/src/lib/inspect.ts +226 -0
  73. package/src/lib/kind-colors.ts +132 -0
  74. package/src/lib/live-table.ts +204 -0
  75. package/src/lib/manifest-health.ts +71 -0
  76. package/src/lib/manifest.ts +139 -0
  77. package/src/lib/metadata.ts +52 -0
  78. package/src/lib/node-metrics.ts +242 -0
  79. package/src/lib/operate.ts +114 -0
  80. package/src/lib/pipeline-flow.ts +120 -0
  81. package/src/lib/rca.ts +193 -0
  82. package/src/lib/telemetry.ts +155 -0
  83. package/src/lib/topology-graph.ts +551 -0
  84. package/src/lib/topology-view.ts +185 -0
  85. package/src/lib/waterfall.ts +148 -0
  86. package/src/main.ts +63 -29
  87. package/src/pages/Errors.vue +272 -0
  88. package/src/pages/Home.stories.ts +7 -8
  89. package/src/pages/Home.vue +255 -540
  90. package/src/pages/Hooks.stories.ts +44 -0
  91. package/src/pages/Hooks.vue +165 -164
  92. package/src/pages/Inspect.vue +240 -0
  93. package/src/pages/Map.vue +187 -0
  94. package/src/pages/Operate.vue +74 -0
  95. package/src/pages/Plugins.stories.ts +1 -1
  96. package/src/pages/Plugins.vue +174 -238
  97. package/src/pages/Projects.vue +62 -60
  98. package/src/pages/Streams.vue +344 -0
  99. package/src/pages/Topology.vue +318 -136
  100. package/src/pages/Trace.vue +174 -412
  101. package/src/pages/__tests__/Home.test.ts +109 -54
  102. package/src/pages/__tests__/Hooks.test.ts +5 -5
  103. package/src/pages/__tests__/Inspect.test.ts +111 -0
  104. package/src/pages/__tests__/Plugins.test.ts +85 -35
  105. package/src/pages/__tests__/Trace.test.ts +117 -0
  106. package/src/pages/operate/CommandsPanel.vue +186 -0
  107. package/src/pages/{Dispatch.vue → operate/DispatchPanel.vue} +100 -203
  108. package/src/pages/operate/EndpointPicker.vue +56 -0
  109. package/src/pages/operate/RunPanel.vue +316 -0
  110. package/src/server/__tests__/nwire-read.test.ts +80 -0
  111. package/src/server/nwire-read.ts +63 -0
  112. package/vite.config.ts +220 -2
  113. package/src/lib/__tests__/normalize-cache.test.ts +0 -105
  114. package/src/lib/cache.ts +0 -312
  115. package/src/lib/normalize-cache.ts +0 -92
  116. package/src/pages/Actions.vue +0 -171
  117. package/src/pages/Apps.vue +0 -177
  118. package/src/pages/Commands.vue +0 -262
  119. package/src/pages/Events.vue +0 -210
  120. package/src/pages/Live.vue +0 -249
  121. package/src/pages/Overview.vue +0 -161
  122. package/src/pages/Projections.vue +0 -148
  123. package/src/pages/Queries.vue +0 -148
  124. package/src/pages/Run.vue +0 -618
  125. package/src/pages/Sinks.vue +0 -124
  126. package/src/pages/TraceNode.vue +0 -164
  127. package/src/pages/Workflows.vue +0 -184
  128. package/src/pages/__tests__/Actions.test.ts +0 -98
  129. package/src/pages/__tests__/Projections.test.ts +0 -90
  130. package/src/pages/__tests__/Queries.test.ts +0 -86
@@ -0,0 +1,272 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Errors / Debug — the friendly RCA surface.
4
+ *
5
+ * /errors?correlationId=…
6
+ *
7
+ * Source picker at the top: Live (stream the current run) or a past run
8
+ * (static snapshot). Subscribes via `useTelemetry`, groups every failure
9
+ * into incidents (`groupIncidents`), and reads each one back in plain language:
10
+ *
11
+ * - Left: a KPI strip (incidents · critical · dead-lettered) over a list of
12
+ * friendly ErrorCards, newest first, filterable by severity.
13
+ * - Right: the selected incident's RcaPanel (what happened · timeline ·
14
+ * impact · evidence), with a SourceDrawer for the failing unit.
15
+ */
16
+ import { computed, ref, watch } from "vue";
17
+ import { useRoute, useRouter } from "vue-router";
18
+ import { ShieldCheck, Bug, Filter, Clock } from "lucide-vue-next";
19
+ import { useTelemetry, useRunList, type TelemetrySource } from "@/composables/useTelemetry";
20
+ import { useManifest } from "@/composables/useManifest";
21
+ import { groupIncidents, type Incident } from "@/lib/rca";
22
+ import type { Severity } from "@/lib/error-friendly";
23
+ import { ErrorCard, RcaPanel, StatusBadge, EmptyState, KpiTile, SourceDrawer } from "@/components";
24
+
25
+ const route = useRoute();
26
+ const router = useRouter();
27
+
28
+ const { records, status, source, connect } = useTelemetry();
29
+ const { runs } = useRunList();
30
+ const { view } = useManifest();
31
+
32
+ // ── Source picker ─────────────────────────────────────────────────────
33
+ const isLive = computed(() => source.value === "live");
34
+
35
+ function selectSource(s: TelemetrySource): void {
36
+ source.value = s;
37
+ }
38
+
39
+ function isActiveRun(runId: string): boolean {
40
+ if (isLive.value) return false;
41
+ const s = source.value;
42
+ return typeof s === "object" && s !== null && s.id === runId;
43
+ }
44
+
45
+ function formatRunId(id: string): string {
46
+ const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}-\d{2}-\d{2})/.exec(id);
47
+ if (m) return `${m[1]} ${m[2]!.replaceAll("-", ":")}`;
48
+ return id;
49
+ }
50
+
51
+ // ── Connection state → StatusBadge ───────────────────────────────────
52
+ const conn = computed(() => {
53
+ if (!isLive.value) return { status: "idle" as const, label: "history", pulse: false };
54
+ switch (status.value) {
55
+ case "open":
56
+ return { status: "live" as const, label: "live", pulse: true };
57
+ case "connecting":
58
+ case "reconnecting":
59
+ return { status: "warn" as const, label: status.value, pulse: false };
60
+ case "closed":
61
+ return { status: "error" as const, label: "closed", pulse: false };
62
+ default:
63
+ return { status: "idle" as const, label: "idle", pulse: false };
64
+ }
65
+ });
66
+
67
+ // ── Incidents (newest first) ─────────────────────────────────────────
68
+ const allIncidents = computed(() => groupIncidents(records.value));
69
+
70
+ const severityFilter = ref<Severity | "all">("all");
71
+ const SEVERITIES: { id: Severity | "all"; label: string }[] = [
72
+ { id: "all", label: "All" },
73
+ { id: "critical", label: "Critical" },
74
+ { id: "high", label: "High" },
75
+ { id: "warning", label: "Warning" },
76
+ ];
77
+
78
+ const incidents = computed(() =>
79
+ severityFilter.value === "all"
80
+ ? allIncidents.value
81
+ : allIncidents.value.filter((i) => i.severity === severityFilter.value),
82
+ );
83
+
84
+ const kpis = computed(() => {
85
+ const all = allIncidents.value;
86
+ return {
87
+ total: all.length,
88
+ critical: all.filter((i) => i.severity === "critical").length,
89
+ deadLettered: all.filter(
90
+ (i) => i.root.kind === "dlq.recorded" || i.root.kind === "reaction.exhausted",
91
+ ).length,
92
+ };
93
+ });
94
+
95
+ // ── Selection (deep-linkable by correlationId) ───────────────────────
96
+ const selectedId = ref<string | undefined>(
97
+ route.query.correlationId ? String(route.query.correlationId) : undefined,
98
+ );
99
+
100
+ const selected = computed<Incident | undefined>(() => {
101
+ const list = incidents.value;
102
+ return list.find((i) => i.id === selectedId.value) ?? list[0];
103
+ });
104
+
105
+ function select(incident: Incident): void {
106
+ selectedId.value = incident.id;
107
+ if (incident.correlationId) {
108
+ void router.replace({ query: { ...route.query, correlationId: incident.correlationId } });
109
+ }
110
+ }
111
+
112
+ // Keep the URL's correlationId in sync if the list re-keys to a new default.
113
+ watch(
114
+ () => route.query.correlationId,
115
+ (cid) => {
116
+ if (cid) selectedId.value = String(cid);
117
+ },
118
+ );
119
+
120
+ // ── Source of the failing unit, resolved from the manifest ───────────
121
+ const NODE_KIND: Record<string, string> = { "external call": "externalCall", operation: "handler" };
122
+
123
+ const selectedSource = computed(() => {
124
+ const s = selected.value?.friendly.subject;
125
+ if (!s || !view.value) return undefined;
126
+ const kind = NODE_KIND[s.kind] ?? s.kind;
127
+ return view.value.node(`${kind}:${s.name}`)?.source ?? undefined;
128
+ });
129
+
130
+ const drawerSource = ref<{ file: string; line?: number; column?: number } | null>(null);
131
+
132
+ const loading = computed(() => status.value === "connecting" && records.value.length === 0);
133
+ </script>
134
+
135
+ <template>
136
+ <div class="h-full flex flex-col">
137
+ <!-- Source picker bar -->
138
+ <div
139
+ class="border-b border-zinc-800 px-4 py-2 flex items-center gap-2"
140
+ data-testid="errors-source-picker"
141
+ >
142
+ <button
143
+ class="h-7 px-2.5 rounded text-xs font-medium border transition-colors"
144
+ :class="
145
+ isLive
146
+ ? 'bg-sky-950/60 border-sky-800 text-sky-300'
147
+ : 'bg-zinc-900 border-zinc-800 text-zinc-400 hover:text-zinc-200'
148
+ "
149
+ data-testid="errors-source-live"
150
+ @click="selectSource('live')"
151
+ >
152
+ Live
153
+ </button>
154
+ <template v-if="runs.length > 0">
155
+ <div class="h-4 w-px bg-zinc-800" />
156
+ <Clock class="w-3 h-3 text-zinc-600 shrink-0" />
157
+ <button
158
+ v-for="run in runs"
159
+ :key="run.id"
160
+ class="h-7 px-2.5 rounded text-xs border transition-colors font-mono"
161
+ :class="
162
+ isActiveRun(run.id)
163
+ ? 'bg-zinc-700 border-zinc-600 text-zinc-100'
164
+ : 'bg-zinc-900 border-zinc-800 text-zinc-500 hover:text-zinc-200'
165
+ "
166
+ :data-testid="`errors-source-run-${run.id}`"
167
+ @click="selectSource(run)"
168
+ >
169
+ {{ formatRunId(run.id) }}
170
+ </button>
171
+ </template>
172
+ </div>
173
+
174
+ <!-- Main layout: left list + right panel -->
175
+ <div class="flex-1 flex min-h-0">
176
+ <!-- ── Left: KPI strip + incident list ──────────────────────────── -->
177
+ <aside class="w-[26rem] border-r border-zinc-800 flex flex-col shrink-0">
178
+ <header class="border-b border-zinc-800 px-4 py-3 flex items-center justify-between gap-2">
179
+ <div class="flex items-center gap-2">
180
+ <Bug class="w-4 h-4 text-rose-400" />
181
+ <h1 class="text-sm font-medium">Errors</h1>
182
+ </div>
183
+ <StatusBadge :status="conn.status" :label="conn.label" :pulse="conn.pulse" />
184
+ </header>
185
+
186
+ <div class="px-3 py-3 grid grid-cols-3 gap-2 border-b border-zinc-800">
187
+ <KpiTile label="Incidents" :value="kpis.total" />
188
+ <KpiTile
189
+ label="Critical"
190
+ :value="kpis.critical"
191
+ :accent="kpis.critical ? '#fb7185' : undefined"
192
+ />
193
+ <KpiTile
194
+ label="Dead-letter"
195
+ :value="kpis.deadLettered"
196
+ :accent="kpis.deadLettered ? '#fb7185' : undefined"
197
+ />
198
+ </div>
199
+
200
+ <div class="px-3 py-2 border-b border-zinc-800 flex items-center gap-1.5">
201
+ <Filter class="w-3 h-3 text-zinc-600" />
202
+ <button
203
+ v-for="s in SEVERITIES"
204
+ :key="s.id"
205
+ class="text-[11px] px-2 py-0.5 rounded transition-colors"
206
+ :class="
207
+ severityFilter === s.id
208
+ ? 'bg-zinc-800 text-zinc-100'
209
+ : 'text-zinc-500 hover:text-zinc-300'
210
+ "
211
+ :data-testid="`sev-filter-${s.id}`"
212
+ @click="severityFilter = s.id"
213
+ >
214
+ {{ s.label }}
215
+ </button>
216
+ </div>
217
+
218
+ <div class="flex-1 overflow-auto p-3 space-y-2">
219
+ <EmptyState
220
+ v-if="allIncidents.length === 0"
221
+ :icon="ShieldCheck"
222
+ title="All clear"
223
+ :hint="
224
+ loading
225
+ ? 'Connecting to the telemetry stream…'
226
+ : 'No failures on the stream. Trigger behavior and anything that breaks will show up here — explained, not just logged.'
227
+ "
228
+ />
229
+ <div v-else-if="incidents.length === 0" class="text-xs text-zinc-500 p-4 text-center">
230
+ No {{ severityFilter }} incidents.
231
+ </div>
232
+ <ErrorCard
233
+ v-for="incident in incidents"
234
+ :key="incident.id"
235
+ :record="incident.root"
236
+ :selected="incident.id === selected?.id"
237
+ @select="select(incident)"
238
+ />
239
+ </div>
240
+ </aside>
241
+
242
+ <!-- ── Right: RCA panel ─────────────────────────────────────────── -->
243
+ <main class="flex-1 flex flex-col min-w-0">
244
+ <RcaPanel
245
+ v-if="selected"
246
+ :incident="selected"
247
+ :source="selectedSource"
248
+ class="flex-1 min-h-0"
249
+ @open-source="drawerSource = $event"
250
+ />
251
+ <EmptyState
252
+ v-else
253
+ :icon="ShieldCheck"
254
+ title="Nothing to debug"
255
+ hint="When a failure lands, pick it on the left to open its root-cause analysis."
256
+ class="m-auto"
257
+ />
258
+ </main>
259
+
260
+ <SourceDrawer :source="drawerSource" @close="drawerSource = null" />
261
+
262
+ <!-- reconnect affordance when the stream drops (live mode only) -->
263
+ <button
264
+ v-if="isLive && status === 'closed'"
265
+ class="fixed bottom-4 right-4 text-xs px-3 py-1.5 rounded bg-zinc-800 text-orange-400 hover:bg-zinc-700"
266
+ @click="connect()"
267
+ >
268
+ Reconnect stream
269
+ </button>
270
+ </div>
271
+ </div>
272
+ </template>
@@ -3,18 +3,17 @@ import { createRouter, createMemoryHistory } from "vue-router";
3
3
  import Home from "./Home.vue";
4
4
 
5
5
  /**
6
- * Storybook for the Home dashboard. We give it an in-memory router so
7
- * the "→ trace" / plugin-row links render without exploding, and let the
8
- * page try to fetch /__nwire/manifest.json + /_nwire/telemetry/* in
9
- * Storybook those fail and the component degrades to its "no live data"
10
- * states, which is exactly what we want to document.
6
+ * Storybook for the Home discovery dashboard. The `/__nwire/*` fetches fail
7
+ * inside Storybook, so the discovered-projects grid degrades to its empty
8
+ * state exactly the surface we want to document.
11
9
  */
12
10
  const router = createRouter({
13
11
  history: createMemoryHistory(),
14
12
  routes: [
15
13
  { path: "/", name: "home", component: Home },
16
14
  { path: "/trace", name: "trace", component: { template: "<div/>" } },
17
- { path: "/hooks", name: "hooks", component: { template: "<div/>" } },
15
+ { path: "/streams", name: "streams", component: { template: "<div/>" } },
16
+ { path: "/projects/:slug/:page", name: "page", component: { template: "<div/>" } },
18
17
  ],
19
18
  });
20
19
 
@@ -43,5 +42,5 @@ export default meta;
43
42
 
44
43
  type Story = StoryObj<typeof Home>;
45
44
 
46
- /** Default: the wire isn't running all three sections degrade gracefully. */
47
- export const NoLiveData: Story = {};
45
+ /** Default: nothing reachable the grid degrades to its empty state. */
46
+ export const NoProjects: Story = {};