loom-spec 0.2.0 → 0.3.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/dist/cli/importTrace.d.ts +15 -0
- package/dist/cli/importTrace.js +188 -0
- package/dist/cli/importTrace.js.map +1 -0
- package/dist/cli/index.js +37 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/server/otel.d.ts +32 -0
- package/dist/server/otel.js +98 -0
- package/dist/server/otel.js.map +1 -0
- package/dist/view/assets/TimelineView-DEfpV9mL.js +16 -0
- package/dist/view/assets/{index-B18EbiQt.css → index-CvyHnPjR.css} +1 -1
- package/dist/view/assets/index-Du05xzao.js +210 -0
- package/dist/view/index.html +2 -2
- package/package.json +1 -1
- package/dist/view/assets/index-DAM9J2qS.js +0 -225
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ImportTraceArgs {
|
|
2
|
+
/** Path to the OTLP JSON trace file. */
|
|
3
|
+
trace: string;
|
|
4
|
+
/** Timeline id to create or append into. */
|
|
5
|
+
asId: string;
|
|
6
|
+
/** Diagram id the new timeline overlays. */
|
|
7
|
+
diagramId: string;
|
|
8
|
+
/** Optional path to a mapping file (see MappingFile shape below). */
|
|
9
|
+
map?: string;
|
|
10
|
+
/** Append to an existing timeline instead of overwriting. */
|
|
11
|
+
append: boolean;
|
|
12
|
+
/** Working directory root (walked up to find .loom/). */
|
|
13
|
+
root: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function runImportTrace(args: ImportTraceArgs): Promise<void>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { findLoomRoot } from "../server/findLoomRoot.js";
|
|
4
|
+
import { readDiagram, readTimeline, writeTimeline } from "../server/fileOps.js";
|
|
5
|
+
import { parseOtlpJson } from "../server/otel.js";
|
|
6
|
+
import { validateTimeline } from "../validate.js";
|
|
7
|
+
const KIND_TO_EVENT_KIND = {
|
|
8
|
+
internal: "compute",
|
|
9
|
+
server: "io",
|
|
10
|
+
client: "io",
|
|
11
|
+
producer: "io",
|
|
12
|
+
consumer: "io",
|
|
13
|
+
unknown: undefined,
|
|
14
|
+
};
|
|
15
|
+
async function loadMapping(path) {
|
|
16
|
+
const raw = await readFile(path, "utf8");
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
return {
|
|
19
|
+
serviceMap: new Map(Object.entries(parsed.services ?? {})),
|
|
20
|
+
spanMap: new Map(Object.entries(parsed.spans ?? {})),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Decide which node a span belongs to.
|
|
25
|
+
* Precedence: explicit span map > explicit service map > heuristic match.
|
|
26
|
+
*/
|
|
27
|
+
function resolveNode(span, mapping, nodes) {
|
|
28
|
+
if (mapping) {
|
|
29
|
+
const direct = mapping.spanMap.get(span.name);
|
|
30
|
+
if (direct)
|
|
31
|
+
return direct;
|
|
32
|
+
if (span.serviceName) {
|
|
33
|
+
const svc = mapping.serviceMap.get(span.serviceName);
|
|
34
|
+
if (svc)
|
|
35
|
+
return svc;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Heuristic: try matching span.name first (it's most specific — for
|
|
39
|
+
// CLIENT/PRODUCER spans it names the *downstream* target, which is the
|
|
40
|
+
// node we want), then fall back to service.name.
|
|
41
|
+
const candidates = [];
|
|
42
|
+
candidates.push(span.name.toLowerCase());
|
|
43
|
+
if (span.serviceName)
|
|
44
|
+
candidates.push(span.serviceName.toLowerCase());
|
|
45
|
+
for (const c of candidates) {
|
|
46
|
+
// 1. exact id match
|
|
47
|
+
const idHit = nodes.find((n) => n.id.toLowerCase() === c);
|
|
48
|
+
if (idHit)
|
|
49
|
+
return idHit.id;
|
|
50
|
+
// 2. node id appears as a token inside the candidate
|
|
51
|
+
// (e.g. "todo-store update" → finds id "todo-store")
|
|
52
|
+
const idInCandidate = nodes.find((n) => c.includes(n.id.toLowerCase()));
|
|
53
|
+
if (idInCandidate)
|
|
54
|
+
return idInCandidate.id;
|
|
55
|
+
// 3. candidate appears in a node label
|
|
56
|
+
// (e.g. "todo-api" → finds label "Todo API")
|
|
57
|
+
const labelHit = nodes.find((n) => n.label.toLowerCase().includes(c));
|
|
58
|
+
if (labelHit)
|
|
59
|
+
return labelHit.id;
|
|
60
|
+
// 4. code-ref path includes the candidate
|
|
61
|
+
const refHit = nodes.find((n) => (n.code_refs ?? []).some((r) => r.path.toLowerCase().includes(c)));
|
|
62
|
+
if (refHit)
|
|
63
|
+
return refHit.id;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function eventIdFor(spanId, existingIds) {
|
|
68
|
+
// Stable: take the first 8 hex chars of the span id, prefix with "ev".
|
|
69
|
+
// Fall back to a sequence if collision (shouldn't happen in practice).
|
|
70
|
+
const base = `ev-${spanId.slice(0, 8) || "span"}`.toLowerCase();
|
|
71
|
+
if (!existingIds.has(base))
|
|
72
|
+
return base;
|
|
73
|
+
let i = 2;
|
|
74
|
+
while (existingIds.has(`${base}-${i}`))
|
|
75
|
+
i++;
|
|
76
|
+
return `${base}-${i}`;
|
|
77
|
+
}
|
|
78
|
+
export async function runImportTrace(args) {
|
|
79
|
+
// 1. Locate the .loom/ root and load the diagram we're overlaying.
|
|
80
|
+
const loomRoot = await findLoomRoot(args.root);
|
|
81
|
+
const diagram = await readDiagram(loomRoot.loomPath, args.diagramId);
|
|
82
|
+
// 2. Load and parse the trace.
|
|
83
|
+
const traceRaw = await readFile(resolve(args.trace), "utf8");
|
|
84
|
+
const traceJson = JSON.parse(traceRaw);
|
|
85
|
+
const spans = parseOtlpJson(traceJson);
|
|
86
|
+
if (spans.length === 0) {
|
|
87
|
+
console.error("Trace contained 0 spans — nothing to import.");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
// 3. Optional mapping file.
|
|
91
|
+
const mapping = args.map ? await loadMapping(resolve(args.map)) : null;
|
|
92
|
+
// 4. Compute t=0 (earliest span start) so the timeline is repo-portable.
|
|
93
|
+
const minStartNs = spans.reduce((m, s) => (s.startNs < m ? s.startNs : m), spans[0].startNs);
|
|
94
|
+
// 5. If appending, load existing timeline; otherwise start fresh.
|
|
95
|
+
let existing = null;
|
|
96
|
+
if (args.append) {
|
|
97
|
+
try {
|
|
98
|
+
existing = await readTimeline(loomRoot.loomPath, args.asId);
|
|
99
|
+
if (existing.diagram !== args.diagramId) {
|
|
100
|
+
console.error(`Refusing to append: existing timeline '${args.asId}' references diagram ` +
|
|
101
|
+
`'${existing.diagram}', not '${args.diagramId}'.`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
if (e.code !== "ENOENT")
|
|
107
|
+
throw e;
|
|
108
|
+
// Fall through: --append on a missing file behaves like create.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const existingIds = new Set(existing?.events.map((e) => e.id) ?? []);
|
|
112
|
+
const spanIdToEventId = new Map();
|
|
113
|
+
const events = [];
|
|
114
|
+
const skipped = [];
|
|
115
|
+
// 6. First pass: pick node + event id for each span.
|
|
116
|
+
for (const s of spans) {
|
|
117
|
+
const node = resolveNode(s, mapping, diagram.nodes);
|
|
118
|
+
if (!node) {
|
|
119
|
+
skipped.push({ span: s, reason: "no matching node" });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const id = eventIdFor(s.spanId, existingIds);
|
|
123
|
+
existingIds.add(id);
|
|
124
|
+
spanIdToEventId.set(s.spanId, id);
|
|
125
|
+
const start_ms = Number((s.startNs - minStartNs) / 1000000n);
|
|
126
|
+
const duration_ms = Number((s.endNs - s.startNs) / 1000000n);
|
|
127
|
+
events.push({
|
|
128
|
+
id,
|
|
129
|
+
node,
|
|
130
|
+
start_ms: Math.max(0, start_ms),
|
|
131
|
+
duration_ms: Math.max(0, duration_ms),
|
|
132
|
+
label: s.name.length > 60 ? s.name.slice(0, 57) + "…" : s.name,
|
|
133
|
+
kind: KIND_TO_EVENT_KIND[s.kind] ?? "compute",
|
|
134
|
+
track: s.serviceName ?? undefined,
|
|
135
|
+
tags: [`otel-import`, `kind:${s.kind}`],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// 7. Second pass: wire triggered_by from span parent.
|
|
139
|
+
for (let i = 0; i < spans.length; i++) {
|
|
140
|
+
const s = spans[i];
|
|
141
|
+
const ev = events.find((e) => e.id === spanIdToEventId.get(s.spanId));
|
|
142
|
+
if (!ev)
|
|
143
|
+
continue;
|
|
144
|
+
if (s.parentSpanId) {
|
|
145
|
+
const parentEventId = spanIdToEventId.get(s.parentSpanId);
|
|
146
|
+
if (parentEventId)
|
|
147
|
+
ev.triggered_by = parentEventId;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// 8. Build the timeline. Auto-derive tracks from distinct services.
|
|
151
|
+
const trackIds = new Set((existing?.tracks?.map((t) => t.id) ?? []).concat(events.map((e) => e.track).filter((t) => !!t)));
|
|
152
|
+
const tracks = Array.from(trackIds).map((id) => ({ id, label: id }));
|
|
153
|
+
const timeline = {
|
|
154
|
+
version: "1",
|
|
155
|
+
id: args.asId,
|
|
156
|
+
title: existing?.title ?? `Imported: ${args.asId}`,
|
|
157
|
+
description: existing?.description ??
|
|
158
|
+
`Generated by 'loom-spec import-trace' from ${args.trace}.`,
|
|
159
|
+
diagram: args.diagramId,
|
|
160
|
+
events: existing ? [...existing.events, ...events] : events,
|
|
161
|
+
tracks,
|
|
162
|
+
};
|
|
163
|
+
// 9. Validate before writing.
|
|
164
|
+
const v = await validateTimeline(timeline);
|
|
165
|
+
if (!v.ok) {
|
|
166
|
+
console.error("Generated timeline failed schema validation:");
|
|
167
|
+
for (const e of v.errors)
|
|
168
|
+
console.error(` - ${e}`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
await writeTimeline(loomRoot.loomPath, args.asId, timeline);
|
|
172
|
+
// 10. Report.
|
|
173
|
+
console.log(`Wrote ${events.length} event${events.length === 1 ? "" : "s"} to ` +
|
|
174
|
+
`${loomRoot.loomPath}/timelines/${args.asId}.timeline.json` +
|
|
175
|
+
(existing ? ` (appended; total ${timeline.events.length})` : ""));
|
|
176
|
+
if (skipped.length > 0) {
|
|
177
|
+
console.log(`Skipped ${skipped.length} span${skipped.length === 1 ? "" : "s"} ` +
|
|
178
|
+
`with no matching node. Pass --map mapping.json to override.`);
|
|
179
|
+
const sample = skipped.slice(0, 5);
|
|
180
|
+
for (const { span, reason } of sample) {
|
|
181
|
+
console.log(` • ${span.name} (service=${span.serviceName ?? "—"}) ${reason}`);
|
|
182
|
+
}
|
|
183
|
+
if (skipped.length > sample.length) {
|
|
184
|
+
console.log(` … and ${skipped.length - sample.length} more.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=importTrace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"importTrace.js","sourceRoot":"","sources":["../../src/cli/importTrace.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { runView } from "./view.js";
|
|
|
4
4
|
import { runValidate } from "./validate.js";
|
|
5
5
|
import { runMcp } from "./mcp.js";
|
|
6
6
|
import { runInstallMcp } from "./installMcp.js";
|
|
7
|
+
import { runImportTrace } from "./importTrace.js";
|
|
7
8
|
const HELP = `loom-spec — node-based architecture spec for your repo
|
|
8
9
|
|
|
9
10
|
Usage:
|
|
@@ -27,11 +28,18 @@ Usage:
|
|
|
27
28
|
pre-commit hook.
|
|
28
29
|
|
|
29
30
|
loom-spec mcp [--root <dir>]
|
|
30
|
-
Start a Model Context Protocol server on stdio. Exposes
|
|
31
|
-
loom_list_diagrams,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
Start a Model Context Protocol server on stdio. Exposes 15 tools
|
|
32
|
+
for diagrams (loom_list_diagrams, loom_add_node, loom_add_edge, …)
|
|
33
|
+
and timelines (loom_list_timelines, loom_add_event, …) — wire it
|
|
34
|
+
into Claude Code's mcp.json (or any MCP-capable client).
|
|
35
|
+
|
|
36
|
+
loom-spec import-trace <trace.json> --as <timeline-id> --diagram <diagram-id>
|
|
37
|
+
[--map <mapping.json>] [--append] [--root <dir>]
|
|
38
|
+
Read an OpenTelemetry OTLP-JSON trace and generate a timeline that
|
|
39
|
+
mirrors the actual spans on the named diagram. Each span becomes
|
|
40
|
+
an event; parent/child relationships preserve as triggered_by;
|
|
41
|
+
service.name becomes the track. Spans whose service or name can't
|
|
42
|
+
be matched to a node are skipped — pass --map to override.
|
|
35
43
|
|
|
36
44
|
loom-spec --help
|
|
37
45
|
Print this help.
|
|
@@ -98,6 +106,30 @@ async function main() {
|
|
|
98
106
|
});
|
|
99
107
|
return;
|
|
100
108
|
}
|
|
109
|
+
if (subcommand === "import-trace") {
|
|
110
|
+
// Positional arg: the trace file path (first non-flag in rest).
|
|
111
|
+
const trace = rest.find((a) => a && !a.startsWith("--"));
|
|
112
|
+
if (!trace) {
|
|
113
|
+
console.error("import-trace: missing trace file path");
|
|
114
|
+
console.log(HELP);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const asId = flags.as;
|
|
118
|
+
const diagramId = flags.diagram;
|
|
119
|
+
if (!asId || !diagramId) {
|
|
120
|
+
console.error("import-trace: --as <timeline-id> and --diagram <diagram-id> are required");
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
await runImportTrace({
|
|
124
|
+
trace,
|
|
125
|
+
asId,
|
|
126
|
+
diagramId,
|
|
127
|
+
map: typeof flags.map === "string" ? flags.map : undefined,
|
|
128
|
+
append: Boolean(flags.append),
|
|
129
|
+
root: flags.root ?? process.cwd(),
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
101
133
|
console.error(`unknown subcommand: ${subcommand}\n`);
|
|
102
134
|
console.log(HELP);
|
|
103
135
|
process.exit(1);
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal OTLP JSON parser. Reads the standard OpenTelemetry resource/scope/
|
|
3
|
+
* spans shape (`resourceSpans[].scopeSpans[].spans[]`) and projects it down
|
|
4
|
+
* to a flat list of normalized spans suitable for translating into a
|
|
5
|
+
* loom-spec timeline.
|
|
6
|
+
*
|
|
7
|
+
* Scope: just enough of the OTLP JSON shape for the import-trace CLI.
|
|
8
|
+
* Does NOT validate exhaustively, does NOT handle protobuf binary, does NOT
|
|
9
|
+
* walk Jaeger or Zipkin formats — those can be follow-ons.
|
|
10
|
+
*/
|
|
11
|
+
export interface ParsedSpan {
|
|
12
|
+
spanId: string;
|
|
13
|
+
parentSpanId: string | null;
|
|
14
|
+
/** Service that emitted the span (from resource attribute). */
|
|
15
|
+
serviceName: string | null;
|
|
16
|
+
/** Span name (often the operation or route). */
|
|
17
|
+
name: string;
|
|
18
|
+
/** "server" | "client" | "internal" | "producer" | "consumer" | "unknown". */
|
|
19
|
+
kind: SpanKind;
|
|
20
|
+
/** Start time in nanoseconds since UNIX epoch. */
|
|
21
|
+
startNs: bigint;
|
|
22
|
+
/** End time in nanoseconds since UNIX epoch. */
|
|
23
|
+
endNs: bigint;
|
|
24
|
+
/** Flattened key→value of attributes (resource + span merged; span wins). */
|
|
25
|
+
attributes: Record<string, string | number | boolean>;
|
|
26
|
+
}
|
|
27
|
+
export type SpanKind = "internal" | "server" | "client" | "producer" | "consumer" | "unknown";
|
|
28
|
+
/**
|
|
29
|
+
* Parse an OTLP JSON object (already JSON.parsed) into normalized spans.
|
|
30
|
+
* Throws if `resourceSpans` is missing — we don't try to recover or guess.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseOtlpJson(raw: unknown): ParsedSpan[];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal OTLP JSON parser. Reads the standard OpenTelemetry resource/scope/
|
|
3
|
+
* spans shape (`resourceSpans[].scopeSpans[].spans[]`) and projects it down
|
|
4
|
+
* to a flat list of normalized spans suitable for translating into a
|
|
5
|
+
* loom-spec timeline.
|
|
6
|
+
*
|
|
7
|
+
* Scope: just enough of the OTLP JSON shape for the import-trace CLI.
|
|
8
|
+
* Does NOT validate exhaustively, does NOT handle protobuf binary, does NOT
|
|
9
|
+
* walk Jaeger or Zipkin formats — those can be follow-ons.
|
|
10
|
+
*/
|
|
11
|
+
const KIND_MAP = {
|
|
12
|
+
0: "unknown",
|
|
13
|
+
1: "internal",
|
|
14
|
+
2: "server",
|
|
15
|
+
3: "client",
|
|
16
|
+
4: "producer",
|
|
17
|
+
5: "consumer",
|
|
18
|
+
};
|
|
19
|
+
function attrValue(a) {
|
|
20
|
+
const v = a.value;
|
|
21
|
+
if (!v)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (v.stringValue !== undefined)
|
|
24
|
+
return v.stringValue;
|
|
25
|
+
if (v.boolValue !== undefined)
|
|
26
|
+
return v.boolValue;
|
|
27
|
+
if (v.intValue !== undefined)
|
|
28
|
+
return Number(v.intValue);
|
|
29
|
+
if (v.doubleValue !== undefined)
|
|
30
|
+
return v.doubleValue;
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
function flattenAttrs(attrs) {
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const a of attrs ?? []) {
|
|
36
|
+
const v = attrValue(a);
|
|
37
|
+
if (v !== undefined)
|
|
38
|
+
out[a.key] = v;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function toBigInt(t) {
|
|
43
|
+
if (t === undefined)
|
|
44
|
+
return 0n;
|
|
45
|
+
if (typeof t === "number")
|
|
46
|
+
return BigInt(t);
|
|
47
|
+
return BigInt(t);
|
|
48
|
+
}
|
|
49
|
+
function normalizeKind(k) {
|
|
50
|
+
if (typeof k === "number")
|
|
51
|
+
return KIND_MAP[k] ?? "unknown";
|
|
52
|
+
if (typeof k === "string") {
|
|
53
|
+
// OTLP exporters sometimes emit "SPAN_KIND_SERVER" etc.
|
|
54
|
+
const lower = k.replace(/^SPAN_KIND_/i, "").toLowerCase();
|
|
55
|
+
if (lower in { internal: 1, server: 1, client: 1, producer: 1, consumer: 1 }) {
|
|
56
|
+
return lower;
|
|
57
|
+
}
|
|
58
|
+
return "unknown";
|
|
59
|
+
}
|
|
60
|
+
return "unknown";
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parse an OTLP JSON object (already JSON.parsed) into normalized spans.
|
|
64
|
+
* Throws if `resourceSpans` is missing — we don't try to recover or guess.
|
|
65
|
+
*/
|
|
66
|
+
export function parseOtlpJson(raw) {
|
|
67
|
+
if (!raw || typeof raw !== "object") {
|
|
68
|
+
throw new Error("Trace file is not a JSON object");
|
|
69
|
+
}
|
|
70
|
+
const root = raw;
|
|
71
|
+
if (!Array.isArray(root.resourceSpans)) {
|
|
72
|
+
throw new Error("Not an OTLP JSON trace — expected top-level 'resourceSpans' array. " +
|
|
73
|
+
"Jaeger/Zipkin formats are not yet supported.");
|
|
74
|
+
}
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const rs of root.resourceSpans) {
|
|
77
|
+
const resourceAttrs = flattenAttrs(rs.resource?.attributes);
|
|
78
|
+
const serviceName = resourceAttrs["service.name"] ?? null;
|
|
79
|
+
for (const ss of rs.scopeSpans ?? []) {
|
|
80
|
+
for (const s of ss.spans ?? []) {
|
|
81
|
+
const spanAttrs = flattenAttrs(s.attributes);
|
|
82
|
+
const attributes = { ...resourceAttrs, ...spanAttrs };
|
|
83
|
+
out.push({
|
|
84
|
+
spanId: s.spanId ?? "",
|
|
85
|
+
parentSpanId: s.parentSpanId && s.parentSpanId.length > 0 ? s.parentSpanId : null,
|
|
86
|
+
serviceName,
|
|
87
|
+
name: s.name ?? "(unnamed)",
|
|
88
|
+
kind: normalizeKind(s.kind),
|
|
89
|
+
startNs: toBigInt(s.startTimeUnixNano),
|
|
90
|
+
endNs: toBigInt(s.endTimeUnixNano),
|
|
91
|
+
attributes,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=otel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel.js","sourceRoot":"","sources":["../../src/server/otel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA2BH,MAAM,QAAQ,GAA6B;IACzC,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,UAAU;IACb,CAAC,EAAE,QAAQ;IACX,CAAC,EAAE,QAAQ;IACX,CAAC,EAAE,UAAU;IACb,CAAC,EAAE,UAAU;CACd,CAAC;AAmCF,SAAS,SAAS,CAAC,CAAgB;IACjC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;IAClB,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACzB,IAAI,CAAC,CAAC,WAAW,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC,WAAW,CAAC;IACtD,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC,SAAS,CAAC;IAClD,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IACxD,IAAI,CAAC,CAAC,WAAW,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC,WAAW,CAAC;IACtD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,YAAY,CAAC,KAAkC;IACtD,MAAM,GAAG,GAA8C,EAAE,CAAC;IAC1D,KAAK,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,EAAE,CAAC;QAC5B,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QACvB,IAAI,CAAC,KAAK,SAAS;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,QAAQ,CAAC,CAA8B;IAC9C,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAC/B,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;IAC5C,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;AACnB,CAAC;AAED,SAAS,aAAa,CAAC,CAA8B;IACnD,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC;IAC3D,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC1B,wDAAwD;QACxD,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QAC1D,IAAI,KAAK,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7E,OAAO,KAAiB,CAAC;QAC3B,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACrD,CAAC;IACD,MAAM,IAAI,GAAG,GAAe,CAAC;IAC7B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CACb,qEAAqE;YACnE,8CAA8C,CACjD,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAiB,EAAE,CAAC;IAC7B,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;QACpC,MAAM,aAAa,GAAG,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAC5D,MAAM,WAAW,GAAI,aAAa,CAAC,cAAc,CAAwB,IAAI,IAAI,CAAC;QAClF,KAAK,MAAM,EAAE,IAAI,EAAE,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;YACrC,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;gBAC/B,MAAM,SAAS,GAAG,YAAY,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;gBAC7C,MAAM,UAAU,GAAG,EAAE,GAAG,aAAa,EAAE,GAAG,SAAS,EAAE,CAAC;gBACtD,GAAG,CAAC,IAAI,CAAC;oBACP,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE;oBACtB,YAAY,EACV,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI;oBACrE,WAAW;oBACX,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,WAAW;oBAC3B,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC;oBAC3B,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC;oBACtC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC;oBAClC,UAAU;iBACX,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import{c as ee,r as l,j as e,s as oe,a as te,l as se,T as de,P as ue,D as me}from"./index-Du05xzao.js";/**
|
|
2
|
+
* @license lucide-react v0.460.0 - ISC
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the ISC license.
|
|
5
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/const fe=ee("Pause",[["rect",{x:"14",y:"4",width:"4",height:"16",rx:"1",key:"zuxfzm"}],["rect",{x:"6",y:"4",width:"4",height:"16",rx:"1",key:"1okwgv"}]]);/**
|
|
7
|
+
* @license lucide-react v0.460.0 - ISC
|
|
8
|
+
*
|
|
9
|
+
* This source code is licensed under the ISC license.
|
|
10
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
11
|
+
*/const he=ee("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);/**
|
|
12
|
+
* @license lucide-react v0.460.0 - ISC
|
|
13
|
+
*
|
|
14
|
+
* This source code is licensed under the ISC license.
|
|
15
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
16
|
+
*/const pe=ee("SkipBack",[["polygon",{points:"19 20 9 12 19 4 19 20",key:"o2sva"}],["line",{x1:"5",x2:"5",y1:"19",y2:"5",key:"1ocqjk"}]]),S=110,M=30,I=52,Y=24,ne=8,ve=4,G=8,Q=10;function xe(s){const o=Math.max(s/10,1),p=Math.floor(Math.log10(o)),h=Math.pow(10,p),u=o/h;let x;return u<1.5?x=h:u<3.5?x=2*h:u<7.5?x=5*h:x=10*h,Math.max(x,1)}function J(s){return s===0?"0":s<1e3?`${s}ms`:s%1e3===0?`${s/1e3}s`:`${(s/1e3).toFixed(2)}s`}function ge(s){const g=[],o=new Set;for(const p of s.tracks??[])o.has(p.id)||(o.add(p.id),g.push(p));for(const p of s.events??[]){const h=p.track??`node:${p.node}`;o.has(h)||(o.add(h),g.push({id:h,label:h}))}return g.length===0&&g.push({id:"default",label:""}),g}function ae(s){return s.track??`node:${s.node}`}function ie(s){return Math.round(s/Q)*Q}function je({timeline:s,diagram:g,nodeTypes:o,selectedEventId:p,onSelectEvent:h,onUpdateEvent:u,playheadMs:x,onScrub:a,zoom:d=1}){const y=l.useRef(null),N=l.useRef(null),[R,B]=l.useState(800),[r,m]=l.useState(null),[c,v]=l.useState(!1);l.useEffect(()=>{const i=y.current;if(!i)return;const t=new ResizeObserver(n=>{const f=n[0];f&&B(f.contentRect.width)});return t.observe(i),B(i.getBoundingClientRect().width),()=>t.disconnect()},[]);const b=l.useMemo(()=>{const i=ge(s),n=(s.events??[]).reduce((E,A)=>Math.max(E,A.start_ms+A.duration_ms),0),f=new Map;i.forEach((E,A)=>f.set(E.id,A));const w=Math.max(R-S-Y,100)*d,C=n>0?w/n:1,K=S+w+Y,D=xe(n/d),F=[];for(let E=0;E<=n+D*.001;E+=D)F.push(E);const V=new Map;g.nodes.forEach(E=>V.set(E.id,E.type));const $=E=>{const A=V.get(E.node),U=A?o.types[A]:void 0;return(U==null?void 0:U.color)??"#71717a"},ce=M+i.length*I+8;return{tracks:i,trackOf:f,totalMs:n,pixelsPerMs:C,svgWidth:K,tickStep:D,ticks:F,colorOf:$,svgHeight:ce}},[s,g,o,R,d]),{tracks:T,trackOf:O,ticks:Z,pixelsPerMs:k,colorOf:X,svgHeight:_,svgWidth:P,totalMs:H}=b,z=i=>{var n;if(!r||r.eventId!==i.id)return{event:i};const t=r.originalEvent;if(r.mode==="move"){const f=Math.max(0,ie(t.start_ms+r.deltaMs)),j=O.get(ae(t))??0,w=Math.min(Math.max(0,j+r.deltaTracks),T.length-1),C=((n=T[w])==null?void 0:n.id)??t.track;return{event:{...t,start_ms:f,track:C}}}else{const f=Math.max(Q,ie(t.duration_ms+r.deltaMs));return{event:{...t,duration_ms:f}}}};l.useEffect(()=>{if(!r)return;const i=n=>{const f=n.clientX-r.startX,j=n.clientY-r.startY,w=f/r.pixelsPerMs,C=Math.round(j/I);m({...r,deltaMs:w,deltaTracks:C})},t=()=>{const n=z(r.originalEvent);u&&(n.event.start_ms!==r.originalEvent.start_ms||n.event.duration_ms!==r.originalEvent.duration_ms||n.event.track!==r.originalEvent.track)&&u(r.eventId,()=>n.event),m(null)};return document.addEventListener("pointermove",i),document.addEventListener("pointerup",t),()=>{document.removeEventListener("pointermove",i),document.removeEventListener("pointerup",t)}},[r,u]),l.useEffect(()=>{if(!c||!a)return;const i=N.current;if(!i)return;const t=f=>{const j=i.getBoundingClientRect(),w=f.clientX-j.left-S,C=Math.max(0,Math.min(H,w/k));a(C)},n=()=>v(!1);return document.addEventListener("pointermove",t),document.addEventListener("pointerup",n),()=>{document.removeEventListener("pointermove",t),document.removeEventListener("pointerup",n)}},[c,a,H,k]);const W=(i,t,n)=>{u&&(i.stopPropagation(),m({mode:n,eventId:t.id,startX:i.clientX,startY:i.clientY,originalEvent:t,pixelsPerMs:k,deltaMs:0,deltaTracks:0}))},q=i=>x===void 0?!1:x>=i.start_ms&&x<=i.start_ms+i.duration_ms,L=x!==void 0?S+x*k:null;return e.jsx("div",{className:"timeline-wrap",ref:y,children:e.jsxs("svg",{ref:N,className:"timeline-svg",width:P,height:_,onClick:i=>{i.target.tagName==="svg"&&h(null)},children:[T.map((i,t)=>{const n=M+t*I;return e.jsxs("g",{children:[i.color&&e.jsx("rect",{x:S,y:n,width:P-S-Y,height:I,fill:i.color,opacity:.4}),e.jsx("line",{x1:0,x2:P,y1:n,y2:n,className:"timeline-lane-sep"}),e.jsx("text",{x:12,y:n+I/2,className:"timeline-track-label",dominantBaseline:"middle",children:i.label})]},`lane-${i.id}`)}),e.jsx("line",{x1:0,x2:P,y1:M+T.length*I,y2:M+T.length*I,className:"timeline-lane-sep"}),e.jsx("rect",{x:S,y:0,width:P-S-Y,height:M,fill:"transparent",style:{cursor:a?"ew-resize":"default"},onPointerDown:i=>{var j;if(!a)return;i.preventDefault();const t=(j=N.current)==null?void 0:j.getBoundingClientRect();if(!t)return;const n=i.clientX-t.left-S,f=Math.max(0,Math.min(H,n/k));a(f),v(!0)}}),e.jsxs("g",{className:"timeline-axis",children:[e.jsx("line",{x1:S,x2:P-Y,y1:M-1,y2:M-1,className:"timeline-axis-line"}),Z.map(i=>{const t=S+i*k;return e.jsxs("g",{children:[e.jsx("line",{x1:t,x2:t,y1:M-6,y2:M,className:"timeline-tick-mark"}),e.jsx("line",{x1:t,x2:t,y1:M,y2:_,className:"timeline-tick-grid"}),e.jsx("text",{x:t,y:M-9,className:"timeline-tick-label",textAnchor:"middle",children:J(i)})]},`tick-${i}`)})]}),(s.events??[]).map(i=>{const{event:t}=z(i),n=O.get(ae(t))??0,f=S+t.start_ms*k,j=Math.max(t.duration_ms*k,ve),w=M+n*I+ne,C=I-ne*2,K=X(t),D=i.id===p,F=q(t),V=(r==null?void 0:r.eventId)===i.id;return e.jsxs("g",{className:`timeline-clip kind-${t.kind??"compute"}${D?" selected":""}${F?" active":""}${V?" ghost":""}`,onClick:$=>{$.stopPropagation(),h(i.id)},children:[e.jsx("rect",{x:f,y:w,width:j,height:C,rx:4,ry:4,fill:K,fillOpacity:F?1:.85,stroke:D?"var(--accent)":K,strokeWidth:D?2:1,style:{cursor:u?"grab":"pointer"},onPointerDown:$=>W($,i,"move")}),u&&j>G&&e.jsx("rect",{x:f+j-G,y:w,width:G,height:C,fill:"transparent",style:{cursor:"ew-resize"},onPointerDown:$=>W($,i,"resize")}),j>40&&t.label&&e.jsx("text",{x:f+6,y:w+C/2,className:"timeline-clip-label",dominantBaseline:"middle",pointerEvents:"none",children:t.label}),e.jsx("title",{children:(t.label??t.node)+` — ${J(t.start_ms)}, ${J(t.duration_ms)} long`})]},i.id)}),L!==null&&e.jsxs("g",{className:"timeline-playhead",pointerEvents:"none",children:[e.jsx("line",{x1:L,x2:L,y1:4,y2:_,className:"timeline-playhead-line"}),e.jsx("polygon",{points:`${L-5},4 ${L+5},4 ${L},14`,className:"timeline-playhead-handle"})]})]})})}function re(s){return s<1e3?`${s} ms`:`${(s/1e3).toFixed(2)} s`}function ye({selectedEvent:s,diagram:g}){if(!s)return e.jsx("div",{className:"inspector",children:e.jsx("div",{className:"empty",children:"Click a clip to inspect"})});const o=g.nodes.find(u=>u.id===s.node),p=s.code_refs??[],h=s.tags??[];return e.jsxs("div",{className:"inspector",children:[e.jsx("span",{className:"type-tag",style:{background:"#71717a",color:"#fff"},children:"EVENT"}),s.label&&e.jsx("h2",{children:s.label}),e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Node"}),e.jsxs("div",{className:"field-value",children:[e.jsx("code",{children:s.node}),o&&e.jsxs("span",{style:{color:"var(--text-muted)"},children:[" · ",o.label]})]})]}),e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Timing"}),e.jsxs("div",{className:"field-value",children:["starts ",re(s.start_ms)," ·"," ","lasts ",re(s.duration_ms)]})]}),s.kind&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Kind"}),e.jsx("div",{className:"field-value",children:s.kind})]}),s.track&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Track"}),e.jsx("div",{className:"field-value",children:e.jsx("code",{children:s.track})})]}),s.triggered_by&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Triggered by"}),e.jsx("div",{className:"field-value",children:e.jsx("code",{children:s.triggered_by})})]}),s.description&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Description"}),e.jsx("div",{className:"field-value",children:s.description})]}),p.length>0&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Code refs"}),p.map((u,x)=>e.jsxs("div",{className:"code-ref",children:[u.path,u.symbol&&e.jsxs("span",{style:{color:"var(--text-muted)"},children:[" · ",u.symbol]}),u.lines&&e.jsxs("span",{style:{color:"var(--text-muted)"},children:[" · L",u.lines]})]},x))]}),h.length>0&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Tags"}),e.jsx("div",{children:h.map(u=>e.jsx("span",{className:"tag",children:u},u))})]}),e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"ID"}),e.jsx("div",{className:"field-value",children:e.jsx("code",{children:s.id})})]})]})}const Ne=[.25,.5,1,2,4];function le(s){return s<1e3?`${Math.round(s)} ms`:`${(s/1e3).toFixed(2)} s`}function ke({playing:s,positionMs:g,totalMs:o,speed:p,onPlayPause:h,onReset:u,onSpeed:x,actions:a}){return e.jsxs("div",{className:"transport-bar",children:[e.jsx("button",{className:"transport-btn",onClick:h,title:s?"Pause (space)":"Play (space)","aria-label":s?"Pause":"Play",children:s?e.jsx(fe,{size:14}):e.jsx(he,{size:14})}),e.jsx("button",{className:"transport-btn",onClick:u,title:"Reset to 0 (home)","aria-label":"Reset",children:e.jsx(pe,{size:14})}),e.jsxs("div",{className:"transport-position",children:[e.jsx("code",{children:le(g)}),e.jsxs("span",{className:"muted",children:[" / ",le(o)]})]}),a,e.jsx("div",{className:"transport-spacer"}),e.jsxs("label",{className:"transport-speed",children:[e.jsx("span",{className:"muted",children:"Speed"}),e.jsx("select",{value:p,onChange:d=>x(Number(d.target.value)),children:Ne.map(d=>e.jsxs("option",{value:d,children:[d,"×"]},d))})]})]})}function be({diagram:s,nodeTypes:g,anchorRef:o,onPick:p,onClose:h}){const u=l.useRef(null),[x,a]=l.useState(null);return l.useLayoutEffect(()=>{function d(){const y=o.current;if(!y)return;const N=y.getBoundingClientRect();a({top:N.bottom+6,right:Math.max(8,window.innerWidth-N.right)})}return d(),window.addEventListener("resize",d),()=>window.removeEventListener("resize",d)},[o]),l.useEffect(()=>{function d(y){var N;if(u.current&&!u.current.contains(y.target)){if((N=o.current)!=null&&N.contains(y.target))return;h()}}return document.addEventListener("mousedown",d),()=>document.removeEventListener("mousedown",d)},[h,o]),x?e.jsxs("div",{className:"add-node-menu",ref:u,style:{top:x.top,right:x.right},children:[e.jsx("div",{className:"add-node-menu-title",children:"Add event on…"}),s.nodes.length===0?e.jsx("div",{className:"switcher-empty",children:"No nodes in this diagram"}):s.nodes.map(d=>{const y=g.types[d.type],N=(y==null?void 0:y.color)??"#71717a";return e.jsxs("button",{className:"add-node-item",onClick:()=>p(d.id),style:{"--node-color":N},children:[e.jsx("span",{className:"add-node-color"}),e.jsxs("div",{className:"add-node-text",children:[e.jsx("div",{className:"add-node-label",children:d.label}),e.jsx("code",{className:"add-node-key",children:d.id})]})]},d.id)})]}):null}const we=500;function Ee(s){const[g,o]=l.useState({timeline:null,diagram:null,nodeTypes:null,loadError:null,saveStatus:"idle",saveError:null,connectionStatus:"connecting"}),p=l.useRef(null),h=l.useRef(null),u=l.useRef(g);u.current=g;const x=l.useRef(!1);l.useEffect(()=>{let r=!1;x.current=!1;async function m(){try{const c=await te(s);if(r)return;const v=await se(c.diagram);if(r)return;h.current=c,x.current=!0,o(b=>({...b,timeline:c,diagram:v.diagram,nodeTypes:v.nodeTypes,loadError:null}))}catch(c){if(r)return;o(v=>({...v,loadError:c instanceof Error?c.message:String(c)}))}}return m(),()=>{r=!0}},[s]);const a=l.useCallback(()=>{p.current&&clearTimeout(p.current),o(r=>({...r,saveStatus:"dirty",saveError:null})),p.current=setTimeout(async()=>{const r=h.current;if(r){o(m=>({...m,saveStatus:"saving"}));try{await oe(r),o(m=>({...m,saveStatus:"saved"}))}catch(m){o(c=>({...c,saveStatus:"error",saveError:m instanceof Error?m.message:String(m)}))}}},we)},[]),d=l.useCallback(r=>{x.current&&(o(m=>{if(!m.timeline)return m;const c=r(m.timeline);return h.current=c,{...m,timeline:c}}),a())},[a]),y=l.useCallback((r,m)=>{d(c=>({...c,events:c.events.map(v=>v.id===r?m(v):v)}))},[d]),N=l.useCallback(r=>{d(m=>({...m,events:[...m.events,r]}))},[d]),R=l.useCallback(r=>{d(m=>({...m,events:m.events.filter(c=>c.id!==r)}))},[d]);return l.useEffect(()=>{o(c=>({...c,connectionStatus:"connecting"}));const r=new EventSource("/api/events");r.onopen=()=>o(c=>({...c,connectionStatus:"connected"})),r.onerror=()=>o(c=>({...c,connectionStatus:"disconnected"}));const m=async()=>{const c=u.current.saveStatus;if(!(c==="dirty"||c==="saving"))try{const v=await te(s),b=await se(v.diagram);h.current=v,o(T=>({...T,timeline:v,diagram:b.diagram,nodeTypes:b.nodeTypes,saveStatus:"idle",saveError:null}))}catch{}};return r.addEventListener("change",c=>{try{const v=JSON.parse(c.data),b=u.current.timeline;(v.type==="timeline-changed"&&v.id===s||v.type==="diagram-changed"&&b&&v.id===b.diagram||v.type==="node-types-changed")&&m()}catch{}}),()=>{r.close()}},[s]),l.useEffect(()=>()=>{p.current&&clearTimeout(p.current)},[]),l.useMemo(()=>({...g,updateEvent:y,addEvent:N,deleteEvent:R}),[g,y,N,R])}function Se(s){const g=new Set(s.events.map(p=>p.id));let o=s.events.length+1;for(;g.has(`ev${o}`);)o++;return`ev${o}`}function Te({id:s,diagrams:g,timelines:o,isDefault:p,onClickHome:h,onNavigate:u,onCreateDiagram:x}){const a=Ee(s),[d,y]=l.useState(null),[N,R]=l.useState(!1),B=l.useRef(null),[r,m]=l.useState(1),[c,v]=l.useState(0),[b,T]=l.useState(!1),[O,Z]=l.useState(1),k=l.useRef(null),X=l.useRef(0),_=l.useMemo(()=>a.timeline?a.timeline.events.reduce((t,n)=>Math.max(t,n.start_ms+n.duration_ms),0):0,[a.timeline]),P=l.useMemo(()=>{const t=new Set;if(!a.timeline)return t;for(const n of a.timeline.events)c>=n.start_ms&&c<=n.start_ms+n.duration_ms&&t.add(n.node);return t},[a.timeline,c]),H=l.useMemo(()=>{const t=new Set;if(!a.diagram)return t;for(const n of a.diagram.edges){const f=n.from.indexOf(":"),j=f===-1?n.from:n.from.slice(0,f);P.has(j)&&t.add(n.id)}return t},[a.diagram,P]);l.useEffect(()=>{if(!b){k.current!==null&&(clearInterval(k.current),k.current=null);return}return X.current=performance.now(),k.current=setInterval(()=>{const t=performance.now(),n=t-X.current;X.current=t,v(f=>{const j=f+n*O;return j>=_?(T(!1),_):j})},16),()=>{k.current!==null&&clearInterval(k.current)}},[b,O,_]);const z=l.useCallback(()=>{_!==0&&T(t=>(!t&&c>=_&&v(0),!t))},[c,_]),W=l.useCallback(()=>{v(0),T(!1)},[]),q=l.useCallback(t=>{v(t)},[]),L=l.useCallback(t=>{const n=a.timeline;if(!n)return;const f=Math.max(0,Math.round(c)),j={id:Se(n),node:t,start_ms:f,duration_ms:200,kind:"compute"};a.addEvent(j),y(j.id),R(!1)},[a,c]);if(l.useEffect(()=>{const t=n=>{const f=n.target;f.tagName==="INPUT"||f.tagName==="TEXTAREA"||f.tagName==="SELECT"||(n.key===" "||n.code==="Space"?(n.preventDefault(),z()):n.key==="Home"?(n.preventDefault(),W()):(n.key==="Backspace"||n.key==="Delete")&&d&&(n.preventDefault(),a.deleteEvent(d),y(null)))};return document.addEventListener("keydown",t),()=>document.removeEventListener("keydown",t)},[z,W,d,a]),a.loadError)return e.jsxs("div",{className:"app timeline-app",children:[e.jsx("div",{className:"topbar",children:e.jsx("div",{className:"title",children:"loom-spec"})}),e.jsx("div",{className:"canvas-wrap",style:{padding:24},children:e.jsxs("code",{style:{color:"var(--status-stale)"},children:["Failed to load: ",a.loadError]})}),e.jsx("div",{className:"inspector",children:e.jsx("div",{className:"empty",children:"—"})})]});if(!a.timeline||!a.diagram||!a.nodeTypes)return e.jsxs("div",{className:"app timeline-app",children:[e.jsx("div",{className:"topbar",children:e.jsx("div",{className:"title",children:"loom-spec"})}),e.jsx("div",{className:"canvas-wrap",style:{padding:24,color:"var(--text-muted)"},children:"Loading timeline…"}),e.jsx("div",{className:"inspector",children:e.jsx("div",{className:"empty",children:"—"})})]});const i=d?a.timeline.events.find(t=>t.id===d)??null:null;return e.jsxs("div",{className:"app timeline-app",children:[e.jsx(de,{viewKind:"timeline",viewId:s,title:a.timeline.title,subtitle:a.timeline.description,diagrams:g,timelines:o,saveStatus:a.saveStatus,saveError:a.saveError,connectionStatus:a.connectionStatus,onClickAdd:()=>{},addMenuOpen:!1,isDefault:p,onClickHome:h,onNavigate:u,onCreateDiagram:x,addButtonRef:null,hideAddButton:!0}),e.jsx(ke,{playing:b,positionMs:c,totalMs:_,speed:O,onPlayPause:z,onReset:W,onSpeed:Z,actions:e.jsxs(e.Fragment,{children:[e.jsxs("button",{ref:B,className:"transport-add-event",onClick:()=>R(t=>!t),title:"Add event at playhead",children:[e.jsx(ue,{size:14})," Event"]}),e.jsxs("label",{className:"transport-zoom",children:[e.jsx("span",{className:"muted",children:"Zoom"}),e.jsxs("select",{value:r,onChange:t=>m(Number(t.target.value)),children:[e.jsx("option",{value:1,children:"1× (fit)"}),e.jsx("option",{value:2,children:"2×"}),e.jsx("option",{value:5,children:"5×"}),e.jsx("option",{value:10,children:"10×"}),e.jsx("option",{value:20,children:"20×"})]})]})]})}),N&&e.jsx(be,{diagram:a.diagram,nodeTypes:a.nodeTypes,anchorRef:B,onPick:L,onClose:()=>R(!1)}),e.jsx("div",{className:"canvas-wrap timeline-canvas-wrap",children:e.jsxs("div",{className:"timeline-split",children:[e.jsx("div",{className:"timeline-split-pane timeline-split-left",children:e.jsx(je,{timeline:a.timeline,diagram:a.diagram,nodeTypes:a.nodeTypes,selectedEventId:d,onSelectEvent:y,onUpdateEvent:a.updateEvent,playheadMs:c,onScrub:q,zoom:r})}),e.jsx("div",{className:"timeline-split-pane timeline-split-right",children:e.jsx(me,{diagram:a.diagram,nodeTypesConfig:a.nodeTypes,interactive:!1,activeNodeIds:P,pulsingEdgeIds:H})})]})}),e.jsx(ye,{selectedEvent:i,diagram:a.diagram})]})}export{Te as TimelineView};
|