@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.
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
package/src/pages/Run.vue DELETED
@@ -1,618 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Studio-as-runner — the Processes panel.
4
- *
5
- * Three columns: topology picker (left), running processes (middle),
6
- * live stdout for the selected process (right). Drives the `/__nwire/run/*`
7
- * supervisor surface served by Studio's Vite middleware.
8
- */
9
- import { computed, onMounted, onUnmounted, ref, watch } from "vue";
10
- import {
11
- Play,
12
- Square,
13
- RefreshCw,
14
- Trash2,
15
- CircleDot,
16
- Circle,
17
- Loader2,
18
- AlertTriangle,
19
- CheckCircle2,
20
- Terminal,
21
- } from "lucide-vue-next";
22
-
23
- type ProcessStatus = "idle" | "starting" | "running" | "stopping" | "exited" | "crashed";
24
-
25
- interface ManagedProcess {
26
- id: string;
27
- topology: string;
28
- port?: number;
29
- startedAt: string;
30
- status: ProcessStatus;
31
- pid?: number;
32
- exitCode?: number | null;
33
- signal?: string | null;
34
- errorMessage?: string;
35
- /** "studio" — spawned via this Studio session.
36
- * "external" — discovered from .nwire/processes/*.json (e.g. nwire dev). */
37
- source?: "studio" | "external";
38
- env?: Record<string, string>;
39
- }
40
-
41
- interface LogLine {
42
- seq: number;
43
- ts: string;
44
- stream: "stdout" | "stderr";
45
- line: string;
46
- }
47
-
48
- const topologies = ref<string[]>([]);
49
- const scripts = ref<Array<{ name: string; command: string }>>([]);
50
- const scriptBusy = ref<string | null>(null);
51
- const processes = ref<ManagedProcess[]>([]);
52
- const selectedId = ref<string | null>(null);
53
- const startTopology = ref<string>("");
54
- const startPort = ref<number>(3000);
55
- const startBusy = ref(false);
56
- const startError = ref<string | null>(null);
57
-
58
- /** KEY=value lines the operator typed; parsed on submit. */
59
- const envInput = ref<string>("");
60
- const envOpen = ref(false);
61
-
62
- function parseEnvInput(raw: string): Record<string, string> {
63
- const out: Record<string, string> = {};
64
- for (const line of raw.split(/\r?\n/)) {
65
- const trimmed = line.trim();
66
- if (!trimmed || trimmed.startsWith("#")) continue;
67
- const eq = trimmed.indexOf("=");
68
- if (eq < 1) continue;
69
- const k = trimmed.slice(0, eq).trim();
70
- const v = trimmed.slice(eq + 1).trim();
71
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) continue;
72
- // Strip matching surrounding quotes — "value" or 'value'.
73
- const dequoted =
74
- (v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))
75
- ? v.slice(1, -1)
76
- : v;
77
- out[k] = dequoted;
78
- }
79
- return out;
80
- }
81
- const autoScroll = ref(true);
82
- const filterStream = ref<"all" | "stdout" | "stderr">("all");
83
-
84
- const logs = ref<LogLine[]>([]);
85
- let es: EventSource | null = null;
86
- let processesPoll: ReturnType<typeof setInterval> | null = null;
87
-
88
- async function loadTopologies() {
89
- try {
90
- const res = await fetch("/__nwire/run/topologies");
91
- const body = (await res.json()) as { topologies: string[] };
92
- topologies.value = body.topologies;
93
- if (!startTopology.value && body.topologies[0]) startTopology.value = body.topologies[0];
94
- } catch {
95
- topologies.value = [];
96
- }
97
- }
98
-
99
- async function loadScripts() {
100
- try {
101
- const res = await fetch("/__nwire/run/scripts");
102
- const body = (await res.json()) as { scripts: Array<{ name: string; command: string }> };
103
- scripts.value = body.scripts;
104
- } catch {
105
- scripts.value = [];
106
- }
107
- }
108
-
109
- /**
110
- * Display ordering for package.json scripts:
111
- *
112
- * 1. `nwire`-prefixed first (alphabetical) — these are the framework's own
113
- * shorthands (`nwire:dev`, `nwire:cache`, etc.) and operators reach for
114
- * them most often.
115
- * 2. Everything else (alphabetical).
116
- *
117
- * Computed off `scripts.value` so refresh updates the order without churn.
118
- */
119
- const sortedScripts = computed(() => {
120
- const nwirePrefixed = scripts.value
121
- .filter((s) => s.name.startsWith("nwire"))
122
- .sort((a, b) => a.name.localeCompare(b.name));
123
- const others = scripts.value
124
- .filter((s) => !s.name.startsWith("nwire"))
125
- .sort((a, b) => a.name.localeCompare(b.name));
126
- return [...nwirePrefixed, ...others];
127
- });
128
-
129
- async function runScript(name: string) {
130
- scriptBusy.value = name;
131
- startError.value = null;
132
- try {
133
- const env = parseEnvInput(envInput.value);
134
- const port = startPort.value > 0 ? startPort.value : undefined;
135
- const res = await fetch("/__nwire/run/exec-script", {
136
- method: "POST",
137
- headers: { "Content-Type": "application/json" },
138
- body: JSON.stringify({ script: name, port, env: Object.keys(env).length ? env : undefined }),
139
- });
140
- const body = (await res.json()) as { process?: ManagedProcess; error?: string };
141
- if (!res.ok) {
142
- startError.value = body.error ?? `run failed (${res.status})`;
143
- return;
144
- }
145
- if (body.process) {
146
- processes.value = [...processes.value, body.process];
147
- selectedId.value = body.process.id;
148
- }
149
- } catch (err) {
150
- startError.value = (err as Error).message;
151
- } finally {
152
- scriptBusy.value = null;
153
- }
154
- }
155
-
156
- async function loadProcesses() {
157
- try {
158
- const res = await fetch("/__nwire/run/processes");
159
- const body = (await res.json()) as { processes: ManagedProcess[] };
160
- processes.value = body.processes;
161
- // If selection died, keep the id but show last logs.
162
- if (selectedId.value && !body.processes.some((p) => p.id === selectedId.value)) {
163
- selectedId.value = null;
164
- }
165
- } catch {
166
- /* ignore */
167
- }
168
- }
169
-
170
- async function start() {
171
- if (!startTopology.value) return;
172
- startBusy.value = true;
173
- startError.value = null;
174
- try {
175
- const res = await fetch("/__nwire/run/start", {
176
- method: "POST",
177
- headers: { "Content-Type": "application/json" },
178
- body: JSON.stringify({
179
- topology: startTopology.value,
180
- port: startPort.value,
181
- env: (() => {
182
- const e = parseEnvInput(envInput.value);
183
- return Object.keys(e).length ? e : undefined;
184
- })(),
185
- }),
186
- });
187
- const body = (await res.json()) as { process?: ManagedProcess; error?: string };
188
- if (!res.ok) {
189
- startError.value = body.error ?? `start failed (${res.status})`;
190
- return;
191
- }
192
- if (body.process) {
193
- processes.value = [...processes.value, body.process];
194
- selectedId.value = body.process.id;
195
- }
196
- } catch (err) {
197
- startError.value = (err as Error).message;
198
- } finally {
199
- startBusy.value = false;
200
- }
201
- }
202
-
203
- async function stop(id: string) {
204
- try {
205
- await fetch(`/__nwire/run/stop/${id}`, { method: "POST" });
206
- } catch {
207
- /* ignore */
208
- }
209
- await loadProcesses();
210
- }
211
-
212
- async function forget(id: string) {
213
- try {
214
- await fetch(`/__nwire/run/forget/${id}`, { method: "POST" });
215
- if (selectedId.value === id) selectedId.value = null;
216
- } catch {
217
- /* ignore */
218
- }
219
- await loadProcesses();
220
- }
221
-
222
- function connectLogStream(id: string | null) {
223
- es?.close();
224
- es = null;
225
- logs.value = [];
226
- if (!id) return;
227
- es = new EventSource(`/__nwire/run/logs/${id}/stream`);
228
- es.onmessage = (msg) => {
229
- try {
230
- const line = JSON.parse(msg.data) as LogLine;
231
- logs.value.push(line);
232
- if (logs.value.length > 5000) logs.value.shift();
233
- } catch {
234
- /* ignore */
235
- }
236
- };
237
- es.onerror = () => {
238
- // SSE auto-reconnects; nothing to do.
239
- };
240
- }
241
-
242
- watch(selectedId, (id) => connectLogStream(id));
243
-
244
- onMounted(() => {
245
- void loadTopologies();
246
- void loadScripts();
247
- void loadProcesses();
248
- processesPoll = setInterval(loadProcesses, 2000);
249
- });
250
-
251
- onUnmounted(() => {
252
- es?.close();
253
- if (processesPoll) clearInterval(processesPoll);
254
- });
255
-
256
- const filteredLogs = computed(() => {
257
- if (filterStream.value === "all") return logs.value;
258
- return logs.value.filter((l) => l.stream === filterStream.value);
259
- });
260
-
261
- const selectedProcess = computed(() => processes.value.find((p) => p.id === selectedId.value));
262
-
263
- /**
264
- * The process Studio's `/_nwire/*` proxy currently routes to — the most
265
- * recently started running process. Surface it in the header so the user
266
- * knows which wire Live / Dispatch / EventStorm are pointing at.
267
- */
268
- const activeProcess = computed<ManagedProcess | undefined>(() => {
269
- const running = processes.value.filter((p) => p.status === "running");
270
- if (running.length === 0) return undefined;
271
- return [...running].sort((a, b) => b.startedAt.localeCompare(a.startedAt))[0];
272
- });
273
-
274
- function shortId(id: string): string {
275
- return id.split("-")[0] ?? id.slice(0, 8);
276
- }
277
-
278
- function timeAgo(iso: string): string {
279
- const s = Math.max(0, Math.floor((Date.now() - new Date(iso).getTime()) / 1000));
280
- if (s < 60) return `${s}s ago`;
281
- if (s < 3600) return `${Math.floor(s / 60)}m ago`;
282
- return `${Math.floor(s / 3600)}h ago`;
283
- }
284
- </script>
285
-
286
- <template>
287
- <div class="h-full flex">
288
- <!-- Topology picker -->
289
- <div class="w-72 border-r border-zinc-800 flex flex-col">
290
- <div class="border-b border-zinc-800 px-4 py-3">
291
- <h1 class="font-semibold text-lg tracking-tight">Run</h1>
292
- <p class="text-xs text-zinc-500 mt-0.5">Spawn a wire from a topology manifest.</p>
293
- <div v-if="activeProcess" class="mt-2 flex items-center gap-1.5 text-[10px] font-mono">
294
- <span class="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
295
- <span class="text-emerald-300">/_nwire/* → :{{ activeProcess.port }}</span>
296
- <span class="text-zinc-600">({{ activeProcess.topology }})</span>
297
- </div>
298
- <div v-else class="mt-2 text-[10px] font-mono text-zinc-600">
299
- /_nwire/* → static fallback
300
- </div>
301
- </div>
302
- <div
303
- v-if="topologies.length === 0 && scripts.length === 0"
304
- class="p-4 text-xs text-zinc-400 space-y-2 border-b border-zinc-800"
305
- >
306
- <div class="flex items-start gap-2 text-amber-300">
307
- <AlertTriangle class="w-3.5 h-3.5 mt-0.5 shrink-0" />
308
- <div class="space-y-1">
309
- <div class="font-medium">No way to start this project</div>
310
- <div class="text-zinc-500">
311
- Studio looks for two things:
312
- <ul class="list-disc list-inside mt-1 space-y-0.5">
313
- <li>
314
- <code class="text-zinc-300">apps/topologies/*.topology.ts</code> — multi-app
315
- projects
316
- </li>
317
- <li>
318
- <code class="text-zinc-300">package.json</code> scripts (<code>dev</code>,
319
- <code>start</code>) — single-app projects
320
- </li>
321
- </ul>
322
- </div>
323
- <div class="text-zinc-500 mt-2">
324
- Most projects use <code>pnpm dev</code>. Add a <code>"dev"</code> script to your
325
- package.json and refresh.
326
- </div>
327
- </div>
328
- </div>
329
- </div>
330
- <div
331
- v-else-if="topologies.length === 0 && scripts.length > 0"
332
- class="p-3 text-[11px] text-zinc-500 border-b border-zinc-800"
333
- >
334
- <div class="flex items-start gap-1.5">
335
- <CircleDot class="w-3 h-3 mt-0.5 text-emerald-400 shrink-0" />
336
- <div>
337
- No topology files. Use the
338
- <span class="text-zinc-300">package.json scripts</span> below — usually
339
- <code class="text-zinc-300">dev</code> is what you want.
340
- </div>
341
- </div>
342
- </div>
343
- <div class="p-4 space-y-3" v-if="topologies.length > 0">
344
- <div>
345
- <label class="text-[10px] uppercase tracking-wide text-zinc-500">Topology</label>
346
- <select
347
- v-model="startTopology"
348
- class="w-full mt-1 bg-zinc-900 border border-zinc-800 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-zinc-600"
349
- >
350
- <option v-for="t in topologies" :key="t" :value="t">{{ t }}</option>
351
- </select>
352
- </div>
353
- <div>
354
- <label class="text-[10px] uppercase tracking-wide text-zinc-500">Port</label>
355
- <input
356
- v-model.number="startPort"
357
- type="number"
358
- class="w-full mt-1 bg-zinc-900 border border-zinc-800 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-zinc-600"
359
- />
360
- </div>
361
- <button
362
- class="w-full px-3 py-2 rounded bg-emerald-700 hover:bg-emerald-600 disabled:opacity-50 text-sm font-medium flex items-center justify-center gap-2"
363
- :disabled="startBusy || !startTopology"
364
- @click="start"
365
- >
366
- <Loader2 v-if="startBusy" class="w-4 h-4 animate-spin" />
367
- <Play v-else class="w-4 h-4" />
368
- {{ startBusy ? "Starting…" : "Start" }}
369
- </button>
370
- <div
371
- v-if="startError"
372
- class="text-xs text-rose-300 bg-rose-950/40 border border-rose-900 rounded p-2 flex items-start gap-2"
373
- >
374
- <AlertTriangle class="w-3 h-3 mt-0.5 shrink-0" />
375
- {{ startError }}
376
- </div>
377
- <button
378
- class="w-full text-[10px] text-zinc-500 hover:text-zinc-300 flex items-center justify-center gap-1"
379
- @click="loadTopologies"
380
- >
381
- <RefreshCw class="w-3 h-3" />
382
- refresh topologies
383
- </button>
384
- </div>
385
- <!-- Env vars + port shared between topology start and script run.
386
- Collapsed by default. Lines like `PORT=4000`, `LOG_LEVEL=debug`
387
- are forwarded to the child process. -->
388
- <div
389
- class="border-t border-zinc-800 px-4 py-3"
390
- v-if="scripts.length > 0 || topologies.length > 0"
391
- >
392
- <button
393
- class="w-full flex items-center justify-between text-[10px] uppercase tracking-wide text-zinc-500 hover:text-zinc-300"
394
- @click="envOpen = !envOpen"
395
- >
396
- <span class="flex items-center gap-1.5">
397
- <span
398
- :class="
399
- envOpen
400
- ? 'rotate-90 inline-block transition-transform'
401
- : 'inline-block transition-transform'
402
- "
403
- >▸</span
404
- >
405
- env + port overrides
406
- </span>
407
- <span
408
- v-if="Object.keys(parseEnvInput(envInput)).length > 0"
409
- class="text-emerald-300 normal-case"
410
- >
411
- {{ Object.keys(parseEnvInput(envInput)).length }} var(s) set
412
- </span>
413
- </button>
414
- <div v-if="envOpen" class="mt-2 space-y-2">
415
- <div>
416
- <label class="text-[10px] uppercase tracking-wide text-zinc-500">Port (PORT env)</label>
417
- <input
418
- v-model.number="startPort"
419
- type="number"
420
- placeholder="auto"
421
- class="w-full mt-1 bg-zinc-900 border border-zinc-800 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-zinc-600"
422
- />
423
- </div>
424
- <div>
425
- <label class="text-[10px] uppercase tracking-wide text-zinc-500"
426
- >Other env (KEY=value, one per line)</label
427
- >
428
- <textarea
429
- v-model="envInput"
430
- rows="4"
431
- placeholder="LOG_LEVEL=debug&#10;NODE_ENV=development"
432
- class="w-full mt-1 bg-zinc-900 border border-zinc-800 rounded px-2 py-1.5 text-xs font-mono focus:outline-none focus:border-zinc-600"
433
- ></textarea>
434
- <div class="text-[10px] text-zinc-600 mt-1">
435
- Applied to both topology starts and script runs.
436
- </div>
437
- </div>
438
- </div>
439
- </div>
440
-
441
- <!-- package.json scripts — fallback for single-app projects without
442
- apps/topologies/. Always shown so multi-app projects can also
443
- use it for one-shot `pnpm test`, `pnpm build`, etc. -->
444
- <div class="border-t border-zinc-800 px-4 py-3" v-if="scripts.length > 0">
445
- <div class="flex items-center justify-between mb-2">
446
- <label class="text-[10px] uppercase tracking-wide text-zinc-500">
447
- package.json scripts
448
- </label>
449
- <button
450
- class="text-zinc-500 hover:text-zinc-200 p-0.5 rounded"
451
- @click="loadScripts"
452
- title="Refresh"
453
- >
454
- <RefreshCw class="w-3 h-3" />
455
- </button>
456
- </div>
457
- <div class="space-y-1">
458
- <button
459
- v-for="s in sortedScripts"
460
- :key="s.name"
461
- class="w-full text-left px-2 py-1.5 rounded hover:bg-zinc-900/70 disabled:opacity-50 flex items-center gap-2 group"
462
- :disabled="scriptBusy === s.name"
463
- @click="runScript(s.name)"
464
- :title="s.command"
465
- >
466
- <Loader2 v-if="scriptBusy === s.name" class="w-3 h-3 animate-spin text-emerald-400" />
467
- <Play v-else class="w-3 h-3 text-zinc-600 group-hover:text-emerald-400" />
468
- <span class="text-xs font-mono text-zinc-200">{{ s.name }}</span>
469
- <span class="text-[10px] text-zinc-600 truncate flex-1 text-right">
470
- {{ s.command }}
471
- </span>
472
- </button>
473
- </div>
474
- </div>
475
- </div>
476
-
477
- <!-- Processes -->
478
- <div class="w-96 border-r border-zinc-800 flex flex-col">
479
- <div class="border-b border-zinc-800 px-4 py-3 flex items-center justify-between">
480
- <div>
481
- <h2 class="font-medium text-sm tracking-tight">Processes</h2>
482
- <p class="text-[10px] text-zinc-500">{{ processes.length }} managed</p>
483
- </div>
484
- <button
485
- class="text-zinc-500 hover:text-zinc-200 p-1 rounded"
486
- @click="loadProcesses"
487
- title="Refresh"
488
- >
489
- <RefreshCw class="w-3.5 h-3.5" />
490
- </button>
491
- </div>
492
- <div class="flex-1 overflow-auto">
493
- <div v-if="processes.length === 0" class="p-6 text-sm text-zinc-500">
494
- No processes. Pick a topology and click <span class="text-zinc-300">Start</span>.
495
- </div>
496
- <button
497
- v-for="p in processes"
498
- :key="p.id"
499
- class="w-full text-left px-4 py-3 border-b border-zinc-900 hover:bg-zinc-900/50 transition-colors"
500
- :class="{ 'bg-zinc-900/70': selectedId === p.id }"
501
- @click="selectedId = p.id"
502
- >
503
- <div class="flex items-center justify-between">
504
- <div class="flex items-center gap-2 min-w-0">
505
- <CircleDot v-if="p.status === 'running'" class="w-3 h-3 text-emerald-400 shrink-0" />
506
- <Loader2
507
- v-else-if="p.status === 'starting' || p.status === 'stopping'"
508
- class="w-3 h-3 text-amber-400 animate-spin shrink-0"
509
- />
510
- <AlertTriangle
511
- v-else-if="p.status === 'crashed'"
512
- class="w-3 h-3 text-rose-400 shrink-0"
513
- />
514
- <CheckCircle2
515
- v-else-if="p.status === 'exited'"
516
- class="w-3 h-3 text-zinc-400 shrink-0"
517
- />
518
- <Circle v-else class="w-3 h-3 text-zinc-500 shrink-0" />
519
- <span class="font-mono text-sm truncate">{{ p.topology }}</span>
520
- <span v-if="p.port" class="text-[10px] text-zinc-500">:{{ p.port }}</span>
521
- </div>
522
- <span class="text-[10px] text-zinc-500">{{ timeAgo(p.startedAt) }}</span>
523
- </div>
524
- <div class="text-[10px] text-zinc-500 mt-0.5 font-mono flex items-center gap-2 flex-wrap">
525
- <span class="uppercase">{{ p.status }}</span>
526
- <span v-if="p.pid">pid {{ p.pid }}</span>
527
- <span class="text-zinc-600">{{ shortId(p.id) }}</span>
528
- <span
529
- v-if="p.source === 'external'"
530
- class="text-[9px] uppercase tracking-wider px-1 py-0 rounded bg-amber-950/40 text-amber-300 border border-amber-900"
531
- title="Discovered from .nwire/processes/*.json — started outside Studio"
532
- >external</span
533
- >
534
- </div>
535
- <div v-if="p.errorMessage" class="text-[10px] text-rose-300 mt-1">
536
- {{ p.errorMessage }}
537
- </div>
538
- </button>
539
- </div>
540
- </div>
541
-
542
- <!-- Logs -->
543
- <div class="flex-1 flex flex-col min-w-0">
544
- <div class="border-b border-zinc-800 px-4 py-3 flex items-center justify-between gap-3">
545
- <div class="flex items-center gap-3 min-w-0">
546
- <Terminal class="w-4 h-4 text-zinc-400 shrink-0" />
547
- <h2 class="font-medium text-sm tracking-tight truncate">
548
- <template v-if="selectedProcess">
549
- {{ selectedProcess.topology }}
550
- <span class="text-zinc-500 font-mono text-xs">{{ shortId(selectedProcess.id) }}</span>
551
- </template>
552
- <template v-else>
553
- <span class="text-zinc-500">Select a process</span>
554
- </template>
555
- </h2>
556
- </div>
557
- <div class="flex items-center gap-2">
558
- <select
559
- v-model="filterStream"
560
- class="bg-zinc-900 border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-200"
561
- >
562
- <option value="all">all</option>
563
- <option value="stdout">stdout</option>
564
- <option value="stderr">stderr</option>
565
- </select>
566
- <label class="flex items-center gap-1 text-[10px] text-zinc-400">
567
- <input type="checkbox" v-model="autoScroll" class="accent-emerald-400" />
568
- tail
569
- </label>
570
- <button
571
- v-if="
572
- selectedProcess &&
573
- (selectedProcess.status === 'running' || selectedProcess.status === 'starting')
574
- "
575
- class="text-xs px-2 py-1 rounded bg-rose-900/40 border border-rose-900 hover:bg-rose-900/70 text-rose-200 flex items-center gap-1"
576
- @click="stop(selectedProcess.id)"
577
- >
578
- <Square class="w-3 h-3" /> stop
579
- </button>
580
- <button
581
- v-if="
582
- selectedProcess &&
583
- (selectedProcess.status === 'exited' || selectedProcess.status === 'crashed')
584
- "
585
- class="text-xs px-2 py-1 rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-300 flex items-center gap-1"
586
- @click="forget(selectedProcess.id)"
587
- >
588
- <Trash2 class="w-3 h-3" /> clear
589
- </button>
590
- </div>
591
- </div>
592
- <div
593
- ref="(el) => el && autoScroll && (el.scrollTop = el.scrollHeight)"
594
- class="flex-1 overflow-auto bg-zinc-950 font-mono text-[11px] leading-tight"
595
- >
596
- <div v-if="!selectedId" class="p-6 text-zinc-500 text-sm font-sans">
597
- Pick a process on the left to view its stdout/stderr.
598
- </div>
599
- <div v-else-if="filteredLogs.length === 0" class="p-6 text-zinc-500 text-sm font-sans">
600
- Waiting for output…
601
- </div>
602
- <div v-else class="p-3 space-y-0.5">
603
- <div
604
- v-for="log in filteredLogs"
605
- :key="log.seq"
606
- class="flex gap-2"
607
- :class="log.stream === 'stderr' ? 'text-rose-200' : 'text-zinc-300'"
608
- >
609
- <span class="text-zinc-600 tabular-nums shrink-0">
610
- {{ new Date(log.ts).toLocaleTimeString() }}
611
- </span>
612
- <span class="whitespace-pre-wrap break-all">{{ log.line }}</span>
613
- </div>
614
- </div>
615
- </div>
616
- </div>
617
- </div>
618
- </template>