@nwire/studio 0.12.0 → 0.13.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.
- package/package.json +5 -3
- package/src/App.vue +62 -56
- package/src/components/BcCard.stories.ts +47 -0
- package/src/components/BcCard.vue +152 -0
- package/src/components/DurationBar.stories.ts +55 -0
- package/src/components/DurationBar.vue +72 -0
- package/src/components/ErrorCard.stories.ts +133 -0
- package/src/components/ErrorCard.vue +153 -0
- package/src/components/GraphCanvas.stories.ts +48 -0
- package/src/components/GraphCanvas.vue +88 -0
- package/src/components/KpiTile.stories.ts +32 -0
- package/src/components/KpiTile.vue +39 -0
- package/src/components/LiveTable.stories.ts +78 -0
- package/src/components/LiveTable.vue +186 -0
- package/src/components/MetadataInspector.stories.ts +53 -0
- package/src/components/MetadataInspector.vue +105 -0
- package/src/components/NodeCard.stories.ts +44 -0
- package/src/components/NodeCard.vue +150 -0
- package/src/components/RcaPanel.stories.ts +95 -0
- package/src/components/RcaPanel.vue +223 -0
- package/src/components/ServiceNode.vue +134 -0
- package/src/components/SourceDrawer.vue +6 -4
- package/src/components/SourcePill.vue +10 -3
- package/src/components/StatusBadge.stories.ts +33 -0
- package/src/components/StatusBadge.vue +54 -0
- package/src/components/Waterfall.stories.ts +85 -0
- package/src/components/Waterfall.vue +53 -0
- package/src/components/WaterfallRow.vue +74 -0
- package/src/components/__tests__/BcCard.test.ts +53 -0
- package/src/components/__tests__/DurationBar.test.ts +31 -0
- package/src/components/__tests__/ErrorCard.test.ts +71 -0
- package/src/components/__tests__/KpiTile.test.ts +23 -0
- package/src/components/__tests__/LiveTable.test.ts +100 -0
- package/src/components/__tests__/MetadataInspector.test.ts +38 -0
- package/src/components/__tests__/NodeCard.test.ts +116 -0
- package/src/components/__tests__/RcaPanel.test.ts +81 -0
- package/src/components/__tests__/StatusBadge.test.ts +23 -0
- package/src/components/__tests__/Waterfall.test.ts +54 -0
- package/src/components/index.ts +13 -0
- package/src/composables/__tests__/composables-context.test.ts +107 -0
- package/src/composables/__tests__/useTelemetry.test.ts +104 -0
- package/src/composables/__tests__/useTelemetryRuns.test.ts +282 -0
- package/src/composables/useDiscovery.ts +73 -0
- package/src/composables/useEndpoints.ts +94 -0
- package/src/composables/useLogTail.ts +51 -0
- package/src/composables/useManifest.ts +43 -0
- package/src/composables/useProcesses.ts +114 -0
- package/src/composables/useProject.ts +34 -0
- package/src/composables/useTelemetry.ts +270 -0
- package/src/lib/__tests__/bc-graph.test.ts +218 -0
- package/src/lib/__tests__/dispatch-form.test.ts +113 -0
- package/src/lib/__tests__/error-friendly.test.ts +198 -0
- package/src/lib/__tests__/home.test.ts +231 -0
- package/src/lib/__tests__/inspect.test.ts +160 -0
- package/src/lib/__tests__/kind-colors.test.ts +59 -0
- package/src/lib/__tests__/live-table.test.ts +194 -0
- package/src/lib/__tests__/manifest-health.test.ts +120 -0
- package/src/lib/__tests__/manifest.test.ts +87 -0
- package/src/lib/__tests__/metadata.test.ts +47 -0
- package/src/lib/__tests__/node-metrics.test.ts +144 -0
- package/src/lib/__tests__/operate.test.ts +97 -0
- package/src/lib/__tests__/pipeline-flow.test.ts +79 -0
- package/src/lib/__tests__/rca.test.ts +124 -0
- package/src/lib/__tests__/telemetry.test.ts +91 -0
- package/src/lib/__tests__/topology-graph.test.ts +331 -0
- package/src/lib/__tests__/topology-view.test.ts +154 -0
- package/src/lib/__tests__/waterfall.test.ts +165 -0
- package/src/lib/bc-graph.ts +298 -0
- package/src/lib/dispatch-form.ts +160 -0
- package/src/lib/error-friendly.ts +288 -0
- package/src/lib/home.ts +191 -0
- package/src/lib/inspect.ts +226 -0
- package/src/lib/kind-colors.ts +132 -0
- package/src/lib/live-table.ts +204 -0
- package/src/lib/manifest-health.ts +71 -0
- package/src/lib/manifest.ts +139 -0
- package/src/lib/metadata.ts +52 -0
- package/src/lib/node-metrics.ts +242 -0
- package/src/lib/operate.ts +114 -0
- package/src/lib/pipeline-flow.ts +120 -0
- package/src/lib/rca.ts +193 -0
- package/src/lib/telemetry.ts +155 -0
- package/src/lib/topology-graph.ts +551 -0
- package/src/lib/topology-view.ts +185 -0
- package/src/lib/waterfall.ts +148 -0
- package/src/main.ts +63 -29
- package/src/pages/Errors.vue +272 -0
- package/src/pages/Home.stories.ts +7 -8
- package/src/pages/Home.vue +255 -540
- package/src/pages/Hooks.stories.ts +44 -0
- package/src/pages/Hooks.vue +165 -164
- package/src/pages/Inspect.vue +240 -0
- package/src/pages/Map.vue +187 -0
- package/src/pages/Operate.vue +74 -0
- package/src/pages/Plugins.stories.ts +1 -1
- package/src/pages/Plugins.vue +174 -238
- package/src/pages/Projects.vue +62 -60
- package/src/pages/Streams.vue +344 -0
- package/src/pages/Topology.vue +318 -136
- package/src/pages/Trace.vue +174 -412
- package/src/pages/__tests__/Home.test.ts +109 -54
- package/src/pages/__tests__/Hooks.test.ts +5 -5
- package/src/pages/__tests__/Inspect.test.ts +111 -0
- package/src/pages/__tests__/Plugins.test.ts +85 -35
- package/src/pages/__tests__/Trace.test.ts +117 -0
- package/src/pages/operate/CommandsPanel.vue +186 -0
- package/src/pages/{Dispatch.vue → operate/DispatchPanel.vue} +100 -203
- package/src/pages/operate/EndpointPicker.vue +56 -0
- package/src/pages/operate/RunPanel.vue +316 -0
- package/src/server/__tests__/nwire-read.test.ts +80 -0
- package/src/server/nwire-read.ts +63 -0
- package/vite.config.ts +220 -2
- package/src/lib/__tests__/normalize-cache.test.ts +0 -105
- package/src/lib/cache.ts +0 -312
- package/src/lib/normalize-cache.ts +0 -92
- package/src/pages/Actions.vue +0 -171
- package/src/pages/Apps.vue +0 -177
- package/src/pages/Commands.vue +0 -262
- package/src/pages/Events.vue +0 -210
- package/src/pages/Live.vue +0 -249
- package/src/pages/Overview.vue +0 -161
- package/src/pages/Projections.vue +0 -148
- package/src/pages/Queries.vue +0 -148
- package/src/pages/Run.vue +0 -618
- package/src/pages/Sinks.vue +0 -124
- package/src/pages/TraceNode.vue +0 -164
- package/src/pages/Workflows.vue +0 -184
- package/src/pages/__tests__/Actions.test.ts +0 -98
- package/src/pages/__tests__/Projections.test.ts +0 -90
- package/src/pages/__tests__/Queries.test.ts +0 -86
package/src/pages/Home.vue
CHANGED
|
@@ -1,599 +1,314 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* Home — the
|
|
3
|
+
* Home — the discovery dashboard. The page you open Studio to: which projects
|
|
4
|
+
* are around, which are running, and what's happening in the one you're on.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Native data only:
|
|
7
|
+
* - `useDiscovery` → the discovered-projects grid (catalog + live health poll)
|
|
8
|
+
* - `useProject` → the active project + a no-reload switch
|
|
9
|
+
* - `useManifest` → the active project's composition (quick-stats strip)
|
|
10
|
+
* - `useTelemetry` → the active project's recent activity + error count
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* When the wire isn't running (proxy 502 / SSE error), the live panels mark
|
|
14
|
-
* themselves "no live data" and the composition panel still renders from the
|
|
15
|
-
* static manifest.
|
|
12
|
+
* Every panel degrades on its own: a project whose manifest isn't generated yet
|
|
13
|
+
* shows pending stats, an unreachable project still lists, and the wire being
|
|
14
|
+
* down just empties the activity feed.
|
|
16
15
|
*/
|
|
17
|
-
import { computed
|
|
16
|
+
import { computed } from "vue";
|
|
18
17
|
import { useRouter } from "vue-router";
|
|
18
|
+
import { RouterLink } from "vue-router";
|
|
19
|
+
import { Home as HomeIcon, FolderSearch, Activity, Map, Radio, Inbox } from "lucide-vue-next";
|
|
20
|
+
import { PageHeader, EmptyState, KpiTile, StatusBadge, KindBadge } from "@/components";
|
|
21
|
+
import { useDiscovery } from "@/composables/useDiscovery";
|
|
22
|
+
import { useProject } from "@/composables/useProject";
|
|
23
|
+
import { useManifest } from "@/composables/useManifest";
|
|
24
|
+
import { useTelemetry } from "@/composables/useTelemetry";
|
|
25
|
+
import { kindColor, recordColorKey } from "@/lib/kind-colors";
|
|
26
|
+
import { projectSlug } from "@/lib/project-catalog";
|
|
19
27
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// ── Telemetry record subset we care about ─────────────────────────────
|
|
33
|
-
interface BaseRecord {
|
|
34
|
-
readonly kind: string;
|
|
35
|
-
readonly appName?: string;
|
|
36
|
-
readonly ts: string;
|
|
37
|
-
}
|
|
38
|
-
interface FailureRecord extends BaseRecord {
|
|
39
|
-
readonly kind: "action.failed" | "dlq.recorded" | "reaction.failed";
|
|
40
|
-
readonly action?: string;
|
|
41
|
-
readonly sourceEvent?: string;
|
|
42
|
-
readonly error?: { message?: string };
|
|
43
|
-
readonly envelope?: { correlationId?: string };
|
|
44
|
-
}
|
|
45
|
-
interface ActionDispatched extends BaseRecord {
|
|
46
|
-
readonly kind: "action.dispatched";
|
|
47
|
-
readonly action: string;
|
|
48
|
-
readonly envelope?: { correlationId?: string };
|
|
49
|
-
}
|
|
50
|
-
interface ActionFailed extends BaseRecord {
|
|
51
|
-
readonly kind: "action.failed";
|
|
52
|
-
readonly action: string;
|
|
53
|
-
readonly envelope?: { correlationId?: string };
|
|
54
|
-
}
|
|
55
|
-
interface HookStep extends BaseRecord {
|
|
56
|
-
readonly kind: "hook.step";
|
|
57
|
-
readonly hookName: string;
|
|
58
|
-
readonly phase: "start" | "end" | "error";
|
|
59
|
-
}
|
|
60
|
-
interface LifecycleRecord extends BaseRecord {
|
|
61
|
-
readonly kind: "lifecycle";
|
|
62
|
-
readonly event: string;
|
|
63
|
-
readonly payload: { pluginName?: string; durationMs?: number; kind?: "plugin" | "module" };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
type TelemetryLike =
|
|
67
|
-
| FailureRecord
|
|
68
|
-
| ActionDispatched
|
|
69
|
-
| ActionFailed
|
|
70
|
-
| HookStep
|
|
71
|
-
| LifecycleRecord
|
|
72
|
-
| BaseRecord;
|
|
28
|
+
sortProjects,
|
|
29
|
+
projectHealth,
|
|
30
|
+
healthBadge,
|
|
31
|
+
healthLabel,
|
|
32
|
+
projectPort,
|
|
33
|
+
projectStats,
|
|
34
|
+
statsArePending,
|
|
35
|
+
quickStats,
|
|
36
|
+
countErrors,
|
|
37
|
+
recentActivity,
|
|
38
|
+
type ProjectStats,
|
|
39
|
+
} from "@/lib/home";
|
|
73
40
|
|
|
74
41
|
const router = useRouter();
|
|
75
|
-
const { cache } = useCache();
|
|
76
|
-
|
|
77
|
-
// ── Live data buffers ─────────────────────────────────────────────────
|
|
78
|
-
const failures = ref<FailureRecord[]>([]);
|
|
79
|
-
const dispatched = ref<ActionDispatched[]>([]);
|
|
80
|
-
const actionFailed = ref<ActionFailed[]>([]);
|
|
81
|
-
const hookSteps = ref<HookStep[]>([]);
|
|
82
|
-
const lifecycle = ref<LifecycleRecord[]>([]);
|
|
83
|
-
|
|
84
|
-
const telemetryStatus = ref<"connecting" | "open" | "closed" | "error">("connecting");
|
|
85
|
-
const recentLoaded = ref(false);
|
|
86
|
-
const recentFailed = ref(false);
|
|
87
|
-
|
|
88
|
-
let es: EventSource | null = null;
|
|
89
|
-
|
|
90
|
-
// Re-render metrics each second so the rolling window slides smoothly.
|
|
91
|
-
const now = ref(Date.now());
|
|
92
|
-
let nowTimer: ReturnType<typeof setInterval> | null = null;
|
|
93
|
-
|
|
94
|
-
onMounted(() => {
|
|
95
|
-
connect();
|
|
96
|
-
void loadRecent();
|
|
97
|
-
nowTimer = setInterval(() => {
|
|
98
|
-
now.value = Date.now();
|
|
99
|
-
}, 1000);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
onUnmounted(() => {
|
|
103
|
-
es?.close();
|
|
104
|
-
if (nowTimer) clearInterval(nowTimer);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
function connect(): void {
|
|
108
|
-
telemetryStatus.value = "connecting";
|
|
109
|
-
es?.close();
|
|
110
|
-
es = new EventSource("/_nwire/telemetry/stream");
|
|
111
|
-
es.onopen = () => {
|
|
112
|
-
telemetryStatus.value = "open";
|
|
113
|
-
};
|
|
114
|
-
es.onerror = () => {
|
|
115
|
-
telemetryStatus.value = "error";
|
|
116
|
-
};
|
|
117
|
-
es.onmessage = (m) => {
|
|
118
|
-
try {
|
|
119
|
-
const rec = JSON.parse(m.data) as TelemetryLike;
|
|
120
|
-
ingest(rec);
|
|
121
|
-
} catch {
|
|
122
|
-
/* ignore non-JSON */
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function loadRecent(): Promise<void> {
|
|
128
|
-
try {
|
|
129
|
-
const res = await fetch("/_nwire/telemetry/recent?limit=500");
|
|
130
|
-
if (!res.ok) {
|
|
131
|
-
recentFailed.value = true;
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
const rows = (await res.json()) as TelemetryLike[];
|
|
135
|
-
for (const r of rows) ingest(r);
|
|
136
|
-
recentLoaded.value = true;
|
|
137
|
-
} catch {
|
|
138
|
-
recentFailed.value = true;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function ingest(rec: TelemetryLike): void {
|
|
143
|
-
switch (rec.kind) {
|
|
144
|
-
case "action.failed":
|
|
145
|
-
case "dlq.recorded":
|
|
146
|
-
case "reaction.failed":
|
|
147
|
-
failures.value.unshift(rec as FailureRecord);
|
|
148
|
-
if (failures.value.length > 200) failures.value.length = 200;
|
|
149
|
-
if (rec.kind === "action.failed") {
|
|
150
|
-
actionFailed.value.push(rec as ActionFailed);
|
|
151
|
-
if (actionFailed.value.length > 2000) actionFailed.value.splice(0, 500);
|
|
152
|
-
}
|
|
153
|
-
break;
|
|
154
|
-
case "action.dispatched":
|
|
155
|
-
dispatched.value.push(rec as ActionDispatched);
|
|
156
|
-
if (dispatched.value.length > 2000) dispatched.value.splice(0, 500);
|
|
157
|
-
break;
|
|
158
|
-
case "hook.step":
|
|
159
|
-
hookSteps.value.push(rec as HookStep);
|
|
160
|
-
if (hookSteps.value.length > 2000) hookSteps.value.splice(0, 500);
|
|
161
|
-
break;
|
|
162
|
-
case "lifecycle":
|
|
163
|
-
lifecycle.value.push(rec as LifecycleRecord);
|
|
164
|
-
if (lifecycle.value.length > 500) lifecycle.value.splice(0, 100);
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
42
|
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
43
|
+
const { projects, runningCount, isLoading, isError, refetch } = useDiscovery();
|
|
44
|
+
const { activeCwd, activeName, switchTo } = useProject();
|
|
45
|
+
const { view } = useManifest(activeCwd);
|
|
46
|
+
const { records } = useTelemetry(activeCwd, { limit: 500 });
|
|
47
|
+
|
|
48
|
+
// ── Discovered projects grid ──────────────────────────────────────────
|
|
49
|
+
const sorted = computed(() => sortProjects(projects.value));
|
|
50
|
+
|
|
51
|
+
// ── Active project quick stats ────────────────────────────────────────
|
|
52
|
+
const errorCount = computed(() => countErrors(records.value));
|
|
53
|
+
const stats = computed(() => quickStats(view.value, errorCount.value));
|
|
54
|
+
|
|
55
|
+
// ── Recent activity ───────────────────────────────────────────────────
|
|
56
|
+
const activity = computed(() => recentActivity(records.value, 8));
|
|
57
|
+
|
|
58
|
+
const statRows: ReadonlyArray<readonly [keyof ProjectStats, string]> = [
|
|
59
|
+
["apps", "apps"],
|
|
60
|
+
["plugins", "plugins"],
|
|
61
|
+
["actions", "actions"],
|
|
62
|
+
["events", "events"],
|
|
63
|
+
["resolvers", "resolvers"],
|
|
64
|
+
["workflows", "workflows"],
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/** Switch the active project (no reload) then jump into one of its pages. */
|
|
68
|
+
function jumpInto(cwd: string, page: "map" | "operate" | "streams"): void {
|
|
69
|
+
if (cwd !== activeCwd.value) switchTo(cwd);
|
|
70
|
+
const slug = projectSlug({ cwd, name: nameFor(cwd) });
|
|
71
|
+
void router.push(`/projects/${slug}/${page}`);
|
|
183
72
|
}
|
|
184
73
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
function inWindow(rec: { ts: string }): boolean {
|
|
189
|
-
return now.value - new Date(rec.ts).getTime() <= WINDOW_MS;
|
|
74
|
+
function nameFor(cwd: string): string {
|
|
75
|
+
return projects.value.find((p) => p.cwd === cwd)?.snapshot.name ?? cwd;
|
|
190
76
|
}
|
|
191
77
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return (n / 60).toFixed(2);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const errorRate = computed(() => {
|
|
198
|
-
const recentDispatched = dispatched.value.filter(inWindow);
|
|
199
|
-
if (recentDispatched.length === 0) return "0.0";
|
|
200
|
-
const failedCorr = new Set(
|
|
201
|
-
actionFailed.value
|
|
202
|
-
.filter(inWindow)
|
|
203
|
-
.map((f) => f.envelope?.correlationId)
|
|
204
|
-
.filter((x): x is string => !!x),
|
|
205
|
-
);
|
|
206
|
-
let bad = 0;
|
|
207
|
-
for (const d of recentDispatched) {
|
|
208
|
-
const id = d.envelope?.correlationId;
|
|
209
|
-
if (id && failedCorr.has(id)) bad++;
|
|
210
|
-
}
|
|
211
|
-
return ((bad / recentDispatched.length) * 100).toFixed(1);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const hottestHooks = computed(() => {
|
|
215
|
-
const counts = new Map<string, number>();
|
|
216
|
-
for (const s of hookSteps.value) {
|
|
217
|
-
if (s.phase !== "end") continue;
|
|
218
|
-
if (!inWindow(s)) continue;
|
|
219
|
-
counts.set(s.hookName, (counts.get(s.hookName) ?? 0) + 1);
|
|
220
|
-
}
|
|
221
|
-
return [...counts.entries()]
|
|
222
|
-
.sort((a, b) => b[1] - a[1])
|
|
223
|
-
.slice(0, 3)
|
|
224
|
-
.map(([name, count]) => ({ name, count }));
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const hasLiveData = computed(() => telemetryStatus.value === "open" || recentLoaded.value);
|
|
228
|
-
|
|
229
|
-
// ── Section 3: composition + boot phases ──────────────────────────────
|
|
230
|
-
const pluginsBootedRecords = computed(() =>
|
|
231
|
-
lifecycle.value.filter((l) => l.event === "nwire.plugin.booted"),
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
const bootSummary = computed(() => {
|
|
235
|
-
const recs = pluginsBootedRecords.value;
|
|
236
|
-
if (recs.length === 0) return null;
|
|
237
|
-
let plugins = 0;
|
|
238
|
-
let totalMs = 0;
|
|
239
|
-
for (const r of recs) {
|
|
240
|
-
totalMs += r.payload.durationMs ?? 0;
|
|
241
|
-
plugins++;
|
|
242
|
-
}
|
|
243
|
-
return { total: recs.length, plugins, totalMs };
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
const compositionStats = computed(() => {
|
|
247
|
-
if (!cache.value) return null;
|
|
248
|
-
return {
|
|
249
|
-
apps: cache.value.apps.length,
|
|
250
|
-
plugins: cache.value.plugins.length,
|
|
251
|
-
actions: cache.value.actions.length,
|
|
252
|
-
events: cache.value.events.length,
|
|
253
|
-
sinks: cache.value.sinks?.length ?? 0,
|
|
254
|
-
};
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
function openPluginHook(name: string): void {
|
|
258
|
-
void router.push({ path: "/hooks", query: { name: `plugin.boot:${name}` } });
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function shortTime(ts: string): string {
|
|
78
|
+
function shortTime(ts?: string): string {
|
|
79
|
+
if (!ts) return "";
|
|
262
80
|
return new Date(ts).toLocaleTimeString(undefined, { hour12: false });
|
|
263
81
|
}
|
|
264
82
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// the first time a user opens Studio. We walk the action manifest, build
|
|
270
|
-
// a best-effort payload from each action's inputSchema (filling required
|
|
271
|
-
// fields with type-appropriate defaults), and POST `/_nwire/dispatch` for
|
|
272
|
-
// each one a few times.
|
|
273
|
-
//
|
|
274
|
-
// All work is fire-and-forget — failures count as "smoke" too (they show
|
|
275
|
-
// up on the failures panel). The button only renders when the cache has
|
|
276
|
-
// actions and the live wire seems reachable.
|
|
277
|
-
|
|
278
|
-
const smokeBusy = ref(false);
|
|
279
|
-
const smokeError = ref<string | null>(null);
|
|
280
|
-
const smokeResult = ref<{ fired: number; failed: number } | null>(null);
|
|
281
|
-
|
|
282
|
-
function sampleFromSchema(schema: unknown): unknown {
|
|
283
|
-
if (!schema || typeof schema !== "object") return {};
|
|
284
|
-
const s = schema as {
|
|
285
|
-
type?: string;
|
|
286
|
-
properties?: Record<string, unknown>;
|
|
287
|
-
required?: string[];
|
|
288
|
-
enum?: unknown[];
|
|
289
|
-
};
|
|
290
|
-
if (s.enum && Array.isArray(s.enum) && s.enum.length > 0) return s.enum[0];
|
|
291
|
-
switch (s.type) {
|
|
292
|
-
case "string":
|
|
293
|
-
return "sample";
|
|
294
|
-
case "number":
|
|
295
|
-
return 1;
|
|
296
|
-
case "boolean":
|
|
297
|
-
return true;
|
|
298
|
-
case "array":
|
|
299
|
-
return [];
|
|
300
|
-
case "object": {
|
|
301
|
-
const out: Record<string, unknown> = {};
|
|
302
|
-
const props = s.properties ?? {};
|
|
303
|
-
const required = new Set(s.required ?? []);
|
|
304
|
-
for (const [key, raw] of Object.entries(props)) {
|
|
305
|
-
// Always include required; otherwise skip — best-effort.
|
|
306
|
-
if (required.has(key)) out[key] = sampleFromSchema(raw);
|
|
307
|
-
}
|
|
308
|
-
return out;
|
|
309
|
-
}
|
|
310
|
-
default:
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async function runSmoke(): Promise<void> {
|
|
316
|
-
if (!cache.value) return;
|
|
317
|
-
smokeBusy.value = true;
|
|
318
|
-
smokeError.value = null;
|
|
319
|
-
smokeResult.value = null;
|
|
320
|
-
let fired = 0;
|
|
321
|
-
let failed = 0;
|
|
322
|
-
try {
|
|
323
|
-
const actions = cache.value.actions;
|
|
324
|
-
if (actions.length === 0) {
|
|
325
|
-
smokeError.value = "No actions in the manifest — nothing to dispatch.";
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
// Fire each action once with sample input; for the first one fire
|
|
329
|
-
// twice so there's at least one repeat in the stream.
|
|
330
|
-
for (let i = 0; i < actions.length; i++) {
|
|
331
|
-
const a = actions[i]!;
|
|
332
|
-
const input = sampleFromSchema(a.inputSchema);
|
|
333
|
-
const repeats = i === 0 ? 2 : 1;
|
|
334
|
-
for (let r = 0; r < repeats; r++) {
|
|
335
|
-
try {
|
|
336
|
-
const res = await fetch("/_nwire/dispatch", {
|
|
337
|
-
method: "POST",
|
|
338
|
-
headers: { "content-type": "application/json" },
|
|
339
|
-
body: JSON.stringify({ handler: a.name, input }),
|
|
340
|
-
});
|
|
341
|
-
if (res.ok) fired++;
|
|
342
|
-
else failed++;
|
|
343
|
-
} catch {
|
|
344
|
-
failed++;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
smokeResult.value = { fired, failed };
|
|
349
|
-
} catch (err) {
|
|
350
|
-
smokeError.value = (err as Error).message;
|
|
351
|
-
} finally {
|
|
352
|
-
smokeBusy.value = false;
|
|
83
|
+
function openActivity(correlationId?: string): void {
|
|
84
|
+
if (!correlationId) {
|
|
85
|
+
void router.push("/streams");
|
|
86
|
+
return;
|
|
353
87
|
}
|
|
88
|
+
void router.push({ path: "/trace", query: { correlationId } });
|
|
354
89
|
}
|
|
355
90
|
</script>
|
|
356
91
|
|
|
357
92
|
<template>
|
|
358
|
-
<div class="
|
|
359
|
-
<div class="
|
|
93
|
+
<div class="h-full overflow-auto" data-testid="home-page">
|
|
94
|
+
<div class="max-w-6xl mx-auto px-6 py-6 space-y-8">
|
|
360
95
|
<PageHeader
|
|
361
96
|
title="Home"
|
|
362
97
|
:icon="HomeIcon"
|
|
363
98
|
icon-color="text-emerald-400"
|
|
364
99
|
:subtitle="
|
|
365
|
-
|
|
366
|
-
?
|
|
367
|
-
: '
|
|
100
|
+
activeName
|
|
101
|
+
? `Discovery dashboard — ${runningCount} running · active: ${activeName}`
|
|
102
|
+
: 'Discovery dashboard — discovered projects and live activity'
|
|
368
103
|
"
|
|
369
104
|
/>
|
|
370
|
-
<div class="flex flex-col items-end gap-1.5 pt-1" data-testid="home-smoke">
|
|
371
|
-
<button
|
|
372
|
-
type="button"
|
|
373
|
-
class="inline-flex items-center gap-2 rounded-md border border-zinc-700 bg-zinc-900 hover:bg-zinc-800 px-3 py-1.5 text-xs font-medium text-zinc-200 transition-colors disabled:opacity-50"
|
|
374
|
-
:disabled="smokeBusy || !cache || cache.actions.length === 0"
|
|
375
|
-
:title="
|
|
376
|
-
!cache || cache.actions.length === 0
|
|
377
|
-
? 'No actions in the manifest yet.'
|
|
378
|
-
: 'Fire a sample dispatch against every action so the live panels populate.'
|
|
379
|
-
"
|
|
380
|
-
@click="runSmoke"
|
|
381
|
-
>
|
|
382
|
-
<Cigarette class="w-3.5 h-3.5 text-amber-400" />
|
|
383
|
-
{{ smokeBusy ? "Smoking…" : "Smoke" }}
|
|
384
|
-
</button>
|
|
385
|
-
<div v-if="smokeResult" class="text-[10px] text-zinc-500 tabular-nums">
|
|
386
|
-
fired {{ smokeResult.fired }} · failed {{ smokeResult.failed }}
|
|
387
|
-
</div>
|
|
388
|
-
<div v-if="smokeError" class="text-[10px] text-rose-300 max-w-[200px] text-right">
|
|
389
|
-
{{ smokeError }}
|
|
390
|
-
</div>
|
|
391
|
-
</div>
|
|
392
|
-
</div>
|
|
393
|
-
|
|
394
|
-
<!-- Section 1: Recent failures ─────────────────────────────────── -->
|
|
395
|
-
<section data-testid="home-failures">
|
|
396
|
-
<div class="flex items-center justify-between mb-2">
|
|
397
|
-
<h2
|
|
398
|
-
class="text-sm font-medium text-zinc-300 uppercase tracking-wide flex items-center gap-2"
|
|
399
|
-
>
|
|
400
|
-
<AlertTriangle class="w-4 h-4 text-rose-400" />
|
|
401
|
-
Recent failures
|
|
402
|
-
</h2>
|
|
403
|
-
<span class="text-[10px] text-zinc-500 tabular-nums">
|
|
404
|
-
{{ recentFailures.length }} shown · {{ failures.length }} total
|
|
405
|
-
</span>
|
|
406
|
-
</div>
|
|
407
105
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
class="
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
<WifiOff class="w-4 h-4" /> No live data — the wire isn't reporting.
|
|
414
|
-
</div>
|
|
415
|
-
<div
|
|
416
|
-
v-else-if="recentFailures.length === 0"
|
|
417
|
-
class="rounded border border-zinc-800 bg-zinc-900/40 px-4 py-6 text-sm text-zinc-400 flex items-center gap-2"
|
|
418
|
-
data-testid="home-failures-empty"
|
|
419
|
-
>
|
|
420
|
-
<CheckCircle2 class="w-4 h-4 text-emerald-400" />
|
|
421
|
-
No failures in the last 10 minutes
|
|
422
|
-
</div>
|
|
423
|
-
<ul v-else class="rounded border border-zinc-800 divide-y divide-zinc-900">
|
|
424
|
-
<li
|
|
425
|
-
v-for="(f, i) in recentFailures"
|
|
426
|
-
:key="`${f.ts}-${i}`"
|
|
427
|
-
class="px-4 py-2 flex items-center gap-3 hover:bg-zinc-900/40 font-mono text-xs"
|
|
428
|
-
data-testid="home-failure-row"
|
|
429
|
-
>
|
|
430
|
-
<span class="text-zinc-600 tabular-nums w-20 shrink-0">
|
|
431
|
-
{{ shortTime(f.ts) }}
|
|
432
|
-
</span>
|
|
433
|
-
<span
|
|
434
|
-
class="text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded shrink-0"
|
|
435
|
-
:class="{
|
|
436
|
-
'bg-rose-950/40 text-rose-300': f.kind === 'action.failed',
|
|
437
|
-
'bg-orange-950/40 text-orange-300': f.kind === 'dlq.recorded',
|
|
438
|
-
'bg-amber-950/40 text-amber-300': f.kind === 'reaction.failed',
|
|
439
|
-
}"
|
|
440
|
-
>{{ f.kind.replace(/\..*$/, "") }}</span
|
|
441
|
-
>
|
|
442
|
-
<span class="text-zinc-100 truncate flex-1">{{ failureLabel(f) }}</span>
|
|
443
|
-
<span class="text-red-400 truncate max-w-[40%]">{{ failureError(f) }}</span>
|
|
444
|
-
<button
|
|
445
|
-
v-if="f.envelope?.correlationId"
|
|
446
|
-
type="button"
|
|
447
|
-
class="text-xs text-emerald-400 hover:text-emerald-300 shrink-0"
|
|
448
|
-
@click="openTrace(f)"
|
|
106
|
+
<!-- Discovered projects ───────────────────────────────────────── -->
|
|
107
|
+
<section data-testid="home-projects">
|
|
108
|
+
<div class="flex items-center justify-between mb-3">
|
|
109
|
+
<h2
|
|
110
|
+
class="text-sm font-medium text-zinc-300 uppercase tracking-wide flex items-center gap-2"
|
|
449
111
|
>
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
112
|
+
<FolderSearch class="w-4 h-4 text-emerald-400" />
|
|
113
|
+
Discovered projects
|
|
114
|
+
<span class="text-[10px] text-zinc-500 normal-case tracking-normal tabular-nums">
|
|
115
|
+
{{ sorted.length }} known · {{ runningCount }} running
|
|
116
|
+
</span>
|
|
117
|
+
</h2>
|
|
118
|
+
<div class="flex items-center gap-2">
|
|
119
|
+
<RouterLink
|
|
120
|
+
to="/projects"
|
|
121
|
+
class="text-xs text-zinc-500 hover:text-zinc-200 px-2 py-1 rounded border border-zinc-800 hover:border-zinc-700"
|
|
122
|
+
data-testid="home-all-projects"
|
|
123
|
+
>
|
|
124
|
+
All projects →
|
|
125
|
+
</RouterLink>
|
|
126
|
+
<button
|
|
127
|
+
class="text-xs text-zinc-500 hover:text-zinc-200 px-2 py-1 rounded border border-zinc-800 hover:border-zinc-700"
|
|
128
|
+
data-testid="home-refresh"
|
|
129
|
+
@click="refetch()"
|
|
130
|
+
>
|
|
131
|
+
Refresh
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
473
135
|
|
|
474
|
-
<div
|
|
475
|
-
v-if="!hasLiveData"
|
|
476
|
-
class="rounded border border-zinc-800 bg-zinc-900/40 px-4 py-6 text-sm text-zinc-500 flex items-center gap-2"
|
|
477
|
-
data-testid="home-metrics-no-live"
|
|
478
|
-
>
|
|
479
|
-
<WifiOff class="w-4 h-4" /> No live data — start the wire and metrics will flow here.
|
|
480
|
-
</div>
|
|
481
|
-
<div v-else class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
482
136
|
<div
|
|
483
|
-
|
|
484
|
-
|
|
137
|
+
v-if="isLoading && sorted.length === 0"
|
|
138
|
+
class="rounded border border-zinc-800 bg-zinc-900/40 px-4 py-8 text-sm text-zinc-500"
|
|
139
|
+
data-testid="home-projects-loading"
|
|
485
140
|
>
|
|
486
|
-
|
|
487
|
-
<div class="text-2xl font-semibold tabular-nums mt-1">{{ reqPerSec }}</div>
|
|
488
|
-
<div class="text-[10px] text-zinc-500 mt-1">actions dispatched ÷ 60</div>
|
|
141
|
+
Discovering projects…
|
|
489
142
|
</div>
|
|
143
|
+
|
|
490
144
|
<div
|
|
491
|
-
|
|
492
|
-
|
|
145
|
+
v-else-if="isError && sorted.length === 0"
|
|
146
|
+
class="rounded border border-rose-900/60 bg-rose-950/20 px-4 py-6 text-sm text-rose-300"
|
|
147
|
+
data-testid="home-projects-error"
|
|
493
148
|
>
|
|
494
|
-
|
|
495
|
-
<
|
|
496
|
-
class="text-2xl font-semibold tabular-nums mt-1"
|
|
497
|
-
:class="Number(errorRate) > 0 ? 'text-rose-300' : 'text-emerald-300'"
|
|
498
|
-
>
|
|
499
|
-
{{ errorRate }}%
|
|
500
|
-
</div>
|
|
501
|
-
<div class="text-[10px] text-zinc-500 mt-1">dispatched → failed by correlationId</div>
|
|
149
|
+
Couldn't reach the discovery endpoint.
|
|
150
|
+
<button class="underline ml-1 hover:text-rose-200" @click="refetch()">Retry</button>
|
|
502
151
|
</div>
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
152
|
+
|
|
153
|
+
<EmptyState
|
|
154
|
+
v-else-if="sorted.length === 0"
|
|
155
|
+
:icon="FolderSearch"
|
|
156
|
+
title="No projects discovered yet"
|
|
157
|
+
hint="Start one with `nwire dev` — it registers itself and shows up here."
|
|
158
|
+
data-testid="home-projects-empty"
|
|
159
|
+
/>
|
|
160
|
+
|
|
161
|
+
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-3" data-testid="home-projects-grid">
|
|
162
|
+
<article
|
|
163
|
+
v-for="p in sorted"
|
|
164
|
+
:key="p.cwd"
|
|
165
|
+
class="border rounded-lg p-4 bg-zinc-950/40 transition-colors"
|
|
166
|
+
:class="p.active ? 'border-emerald-800/70' : 'border-zinc-800 hover:border-zinc-700'"
|
|
167
|
+
data-testid="home-project-card"
|
|
168
|
+
:data-cwd="p.cwd"
|
|
169
|
+
>
|
|
170
|
+
<div class="flex items-start justify-between gap-3">
|
|
171
|
+
<div class="min-w-0">
|
|
172
|
+
<div class="flex items-center gap-2">
|
|
173
|
+
<h3
|
|
174
|
+
class="font-semibold text-sm tracking-tight truncate"
|
|
175
|
+
data-testid="home-project-name"
|
|
176
|
+
>
|
|
177
|
+
{{ p.snapshot.name }}
|
|
178
|
+
</h3>
|
|
179
|
+
<span
|
|
180
|
+
v-if="p.active"
|
|
181
|
+
class="text-[10px] uppercase tracking-wider text-emerald-400 px-1.5 py-0.5 rounded bg-emerald-950/40 border border-emerald-900"
|
|
182
|
+
>
|
|
183
|
+
active
|
|
184
|
+
</span>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="text-[11px] font-mono text-zinc-600 mt-0.5 truncate" :title="p.cwd">
|
|
187
|
+
{{ p.cwd }}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
<StatusBadge
|
|
191
|
+
:status="healthBadge(projectHealth(p))"
|
|
192
|
+
:pulse="projectHealth(p) === 'running'"
|
|
193
|
+
:label="
|
|
194
|
+
projectHealth(p) === 'running' && projectPort(p)
|
|
195
|
+
? `Running :${projectPort(p)}`
|
|
196
|
+
: healthLabel(projectHealth(p))
|
|
197
|
+
"
|
|
198
|
+
data-testid="home-project-health"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<!-- Quick stats from the project's snapshot -->
|
|
203
|
+
<div
|
|
204
|
+
v-if="statsArePending(p)"
|
|
205
|
+
class="mt-4 text-[11px] text-zinc-600"
|
|
206
|
+
data-testid="home-project-stats-pending"
|
|
515
207
|
>
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
208
|
+
No composition yet — open it or run `nwire cache`.
|
|
209
|
+
</div>
|
|
210
|
+
<dl
|
|
211
|
+
v-else
|
|
212
|
+
class="grid grid-cols-3 gap-y-2 gap-x-3 mt-4"
|
|
213
|
+
data-testid="home-project-stats"
|
|
214
|
+
>
|
|
215
|
+
<div v-for="[key, label] in statRows" :key="key">
|
|
216
|
+
<dt class="text-zinc-600 text-[10px] uppercase tracking-wide">{{ label }}</dt>
|
|
217
|
+
<dd
|
|
218
|
+
class="text-sm tabular-nums"
|
|
219
|
+
:style="{ color: kindColor(key.replace(/s$/, '')) }"
|
|
220
|
+
>
|
|
221
|
+
{{ projectStats(p)[key] }}
|
|
222
|
+
</dd>
|
|
223
|
+
</div>
|
|
224
|
+
</dl>
|
|
225
|
+
|
|
226
|
+
<!-- Jump-in actions -->
|
|
227
|
+
<div class="flex items-center gap-2 mt-4 pt-3 border-t border-zinc-900">
|
|
228
|
+
<button
|
|
229
|
+
class="text-[11px] text-zinc-300 hover:text-zinc-100 flex items-center gap-1.5 px-2 py-1 rounded border border-zinc-800 hover:border-zinc-700"
|
|
230
|
+
data-testid="home-jump-map"
|
|
231
|
+
@click="jumpInto(p.cwd, 'map')"
|
|
232
|
+
>
|
|
233
|
+
<Map class="w-3 h-3" /> Map
|
|
234
|
+
</button>
|
|
235
|
+
<button
|
|
236
|
+
class="text-[11px] text-zinc-300 hover:text-zinc-100 flex items-center gap-1.5 px-2 py-1 rounded border border-zinc-800 hover:border-zinc-700"
|
|
237
|
+
data-testid="home-jump-operate"
|
|
238
|
+
@click="jumpInto(p.cwd, 'operate')"
|
|
239
|
+
>
|
|
240
|
+
<Inbox class="w-3 h-3" /> Operate
|
|
241
|
+
</button>
|
|
242
|
+
<button
|
|
243
|
+
class="text-[11px] text-zinc-300 hover:text-zinc-100 flex items-center gap-1.5 px-2 py-1 rounded border border-zinc-800 hover:border-zinc-700"
|
|
244
|
+
data-testid="home-jump-streams"
|
|
245
|
+
@click="jumpInto(p.cwd, 'streams')"
|
|
246
|
+
>
|
|
247
|
+
<Radio class="w-3 h-3" /> Streams
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
</article>
|
|
521
251
|
</div>
|
|
522
|
-
</
|
|
523
|
-
</section>
|
|
252
|
+
</section>
|
|
524
253
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
<div class="flex items-center justify-between mb-2">
|
|
254
|
+
<!-- Active project quick stats ─────────────────────────────────── -->
|
|
255
|
+
<section v-if="activeCwd" data-testid="home-quickstats">
|
|
528
256
|
<h2
|
|
529
|
-
class="text-sm font-medium text-zinc-300 uppercase tracking-wide flex items-center gap-2"
|
|
257
|
+
class="text-sm font-medium text-zinc-300 uppercase tracking-wide flex items-center gap-2 mb-3"
|
|
530
258
|
>
|
|
531
|
-
<
|
|
532
|
-
|
|
259
|
+
<Activity class="w-4 h-4 text-emerald-400" />
|
|
260
|
+
{{ activeName }} — at a glance
|
|
533
261
|
</h2>
|
|
534
|
-
|
|
262
|
+
<div class="flex flex-wrap gap-3">
|
|
263
|
+
<KpiTile
|
|
264
|
+
v-for="s in stats"
|
|
265
|
+
:key="s.label"
|
|
266
|
+
:label="s.label"
|
|
267
|
+
:value="s.value"
|
|
268
|
+
:accent="s.label === 'Errors' && s.value > 0 ? '#fb7185' : kindColor(s.kind)"
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
</section>
|
|
535
272
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
/>
|
|
541
|
-
<div v-else class="space-y-3">
|
|
542
|
-
<div
|
|
543
|
-
v-if="bootSummary"
|
|
544
|
-
class="rounded-lg border border-zinc-800 bg-zinc-900/50 px-4 py-3 text-sm"
|
|
545
|
-
data-testid="home-boot-summary"
|
|
273
|
+
<!-- Recent activity ─────────────────────────────────────────────── -->
|
|
274
|
+
<section v-if="activeCwd" data-testid="home-activity">
|
|
275
|
+
<h2
|
|
276
|
+
class="text-sm font-medium text-zinc-300 uppercase tracking-wide flex items-center gap-2 mb-3"
|
|
546
277
|
>
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
plugins recorded) in
|
|
552
|
-
<span class="tabular-nums">{{ bootSummary.totalMs.toFixed(0) }}</span> ms
|
|
553
|
-
</div>
|
|
278
|
+
<Radio class="w-4 h-4 text-cyan-400" />
|
|
279
|
+
Recent activity
|
|
280
|
+
</h2>
|
|
281
|
+
|
|
554
282
|
<div
|
|
555
|
-
v-
|
|
556
|
-
class="rounded
|
|
557
|
-
data-testid="home-
|
|
283
|
+
v-if="activity.length === 0"
|
|
284
|
+
class="rounded border border-zinc-800 bg-zinc-900/40 px-4 py-6 text-sm text-zinc-500"
|
|
285
|
+
data-testid="home-activity-empty"
|
|
558
286
|
>
|
|
559
|
-
|
|
560
|
-
<span class="tabular-nums">{{ compositionStats.plugins }}</span> plugin(s) ·
|
|
561
|
-
<span class="tabular-nums">{{ compositionStats.actions }}</span> actions ·
|
|
562
|
-
<span class="tabular-nums">{{ compositionStats.events }}</span> events ·
|
|
563
|
-
<span class="tabular-nums">{{ compositionStats.sinks }}</span> sinks
|
|
564
|
-
<div v-if="!hasLiveData" class="text-[11px] text-zinc-500 mt-1">
|
|
565
|
-
Boot timings appear once the wire is running.
|
|
566
|
-
</div>
|
|
287
|
+
No activity yet — dispatch something or start the wire.
|
|
567
288
|
</div>
|
|
568
|
-
|
|
569
289
|
<ul
|
|
570
|
-
v-
|
|
290
|
+
v-else
|
|
571
291
|
class="rounded border border-zinc-800 divide-y divide-zinc-900"
|
|
572
|
-
data-testid="home-
|
|
292
|
+
data-testid="home-activity-list"
|
|
573
293
|
>
|
|
574
294
|
<li
|
|
575
|
-
v-for="(
|
|
576
|
-
:key="`${
|
|
577
|
-
class="px-4 py-
|
|
295
|
+
v-for="(a, i) in activity"
|
|
296
|
+
:key="`${a.ts}-${i}`"
|
|
297
|
+
class="px-4 py-2 flex items-center gap-3 hover:bg-zinc-900/40 font-mono text-xs cursor-pointer"
|
|
298
|
+
data-testid="home-activity-row"
|
|
299
|
+
@click="openActivity(a.correlationId)"
|
|
578
300
|
>
|
|
579
|
-
<span
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
>
|
|
584
|
-
|
|
585
|
-
type="button"
|
|
586
|
-
class="flex-1 text-left text-zinc-100 truncate hover:text-emerald-300"
|
|
587
|
-
@click="openPluginHook(p.payload.pluginName ?? 'unknown')"
|
|
588
|
-
>
|
|
589
|
-
{{ p.payload.pluginName ?? "(unnamed)" }}
|
|
590
|
-
</button>
|
|
591
|
-
<span class="text-zinc-500 tabular-nums shrink-0">
|
|
592
|
-
{{ (p.payload.durationMs ?? 0).toFixed(1) }} ms
|
|
301
|
+
<span class="text-zinc-600 tabular-nums w-20 shrink-0">{{ shortTime(a.ts) }}</span>
|
|
302
|
+
<KindBadge :style="{ color: kindColor(recordColorKey(a.kind)) }">
|
|
303
|
+
{{ a.kind }}
|
|
304
|
+
</KindBadge>
|
|
305
|
+
<span class="flex-1 truncate" :class="a.failed ? 'text-rose-300' : 'text-zinc-100'">
|
|
306
|
+
{{ a.label }}
|
|
593
307
|
</span>
|
|
308
|
+
<span v-if="a.correlationId" class="text-emerald-400 shrink-0">→ trace</span>
|
|
594
309
|
</li>
|
|
595
310
|
</ul>
|
|
596
|
-
</
|
|
597
|
-
</
|
|
311
|
+
</section>
|
|
312
|
+
</div>
|
|
598
313
|
</div>
|
|
599
314
|
</template>
|