OR-1 dataflow CPU sketch
1import cytoscape from "cytoscape";
2import elk from "cytoscape-elk";
3import svg from "cytoscape-svg";
4import type {
5 MonitorNode, MonitorEdge, MonitorUpdate, GraphLoadedMessage,
6 ResetMessage, ServerMessage, SystemState,
7} from "./types";
8import { monitorStylesheet } from "./style";
9import { logicalLayout, physicalLayout } from "@common/layout";
10import { exportSvg, exportPng, copyPng } from "@common/export";
11import { createConnection } from "./connection";
12import type { MonitorConnection } from "./connection";
13import { clearEventLog, updateEventLog, setEventFilter, updateStateInspector, displayNodeMatchingStore, displaySMNodeState } from "./panels";
14import { rp } from "./palette";
15
16// Register plugins
17cytoscape.use(elk);
18cytoscape.use(svg);
19
20const ROUTING_CATEGORY = "routing";
21
22// Global state
23let cy: cytoscape.Core;
24let isPhysicalLayout = true;
25let currentState: SystemState | null = null;
26let currentNodes: ReadonlyArray<MonitorNode> = [];
27let currentEdges: ReadonlyArray<MonitorEdge> = [];
28
29/**
30 * Route edges with intelligent bezier curves to avoid node collisions.
31 * Ported from dfgraph/frontend/src/main.ts.
32 */
33function routeEdges(selector?: string): void {
34 const nodes = cy.nodes().not(":parent").not(".port-node");
35 const nodePositions: Array<{ x: number; y: number }> = [];
36 nodes.forEach((n) => {
37 nodePositions.push({ x: n.position("x"), y: n.position("y") });
38 });
39
40 let leftCount = 0;
41 let rightCount = 0;
42
43 const edges = selector ? cy.edges(selector) : cy.edges();
44 edges.forEach((edge) => {
45 const sy = edge.source().position("y");
46 const ty = edge.target().position("y");
47 const sx = edge.source().position("x");
48 const tx = edge.target().position("x");
49 const span = Math.abs(ty - sy);
50
51 if (span < 80) {
52 edge.style({ "curve-style": "bezier" });
53 return;
54 }
55
56 const blocked = nodePositions.some((p) => {
57 return p.y > Math.min(sy, ty) + 25 && p.y < Math.max(sy, ty) - 25;
58 });
59
60 if (!blocked) {
61 edge.style({ "curve-style": "bezier" });
62 return;
63 }
64
65 const avgX = (sx + tx) / 2;
66 const centerX = (Math.min(...nodePositions.map((p) => p.x)) + Math.max(...nodePositions.map((p) => p.x))) / 2;
67 const goLeft = avgX >= centerX;
68
69 const baseOffset = 30;
70 const stagger = goLeft ? 12 * leftCount : 12 * rightCount;
71 const offset = (goLeft ? -1 : 1) * (baseOffset + stagger);
72
73 if (goLeft) leftCount++;
74 else rightCount++;
75
76 edge.style({
77 "curve-style": "unbundled-bezier",
78 "control-point-distances": [offset * 0.6, offset, offset * 0.6],
79 "control-point-weights": [0.2, 0.5, 0.8],
80 });
81 });
82}
83
84/**
85 * Build Cytoscape elements for logical view (flat, no PE clustering).
86 */
87function buildLogicalElements(
88 nodes: ReadonlyArray<MonitorNode>,
89 edges: ReadonlyArray<MonitorEdge>,
90): cytoscape.ElementDefinition[] {
91 const elements: cytoscape.ElementDefinition[] = [];
92
93 for (const node of nodes) {
94 const label = node.label
95 ? node.label
96 : node.const !== null
97 ? `${node.opcode}\n${node.const}`
98 : node.opcode;
99
100 elements.push({
101 data: {
102 id: node.id,
103 label,
104 colour: node.colour,
105 category: node.category,
106 pe: node.pe,
107 iram_offset: node.iram_offset,
108 ctx: node.ctx,
109 synthetic: node.synthetic ?? false,
110 sm_id: (node as any).sm_id ?? null,
111 },
112 });
113 }
114
115 for (const edge of edges) {
116 const sourceNode = nodes.find((n) => n.id === edge.source);
117 let sourceLabel: string | undefined;
118 if (sourceNode && sourceNode.category === ROUTING_CATEGORY && edge.source_port) {
119 sourceLabel = edge.source_port === "L" ? "T" : "F";
120 }
121
122 elements.push({
123 data: {
124 id: `${edge.source}->${edge.target}:${edge.port}`,
125 source: edge.source,
126 target: edge.target,
127 targetLabel: edge.port,
128 sourceLabel: sourceLabel ?? "",
129 },
130 classes: edge.synthetic ? "synthetic" : undefined,
131 });
132 }
133
134 return elements;
135}
136
137/**
138 * Build Cytoscape elements for physical view with bus topology.
139 * Ported from dfgraph/frontend/src/main.ts buildPhysicalElements().
140 */
141function buildPhysicalElements(
142 nodes: ReadonlyArray<MonitorNode>,
143 edges: ReadonlyArray<MonitorEdge>,
144): cytoscape.ElementDefinition[] {
145 const elements: cytoscape.ElementDefinition[] = [];
146
147 // Build edge target set for seed const detection
148 const edgeTargets = new Set<string>();
149 for (const edge of edges) {
150 edgeTargets.add(edge.target);
151 }
152
153 const seedConstIds = new Set<string>();
154 const nodePeMap = new Map<string, number | null>();
155 const peIds = new Set<number>();
156
157 for (const node of nodes) {
158 nodePeMap.set(node.id, node.pe);
159 if (node.pe !== null) peIds.add(node.pe);
160
161 if (node.category === "config" && node.opcode === "const" && !edgeTargets.has(node.id)) {
162 seedConstIds.add(node.id);
163 }
164 }
165
166 // Create PE cluster parent nodes
167 for (const peId of peIds) {
168 elements.push({
169 data: { id: `pe-${peId}`, label: `PE ${peId}` },
170 classes: "pe-cluster",
171 });
172 }
173
174 // Create operation nodes
175 for (const node of nodes) {
176 const isSeedConst = seedConstIds.has(node.id);
177 const label = node.label
178 ? node.label
179 : node.const !== null
180 ? `${node.opcode}\n${node.const}`
181 : node.opcode;
182
183 const el: cytoscape.ElementDefinition = {
184 data: {
185 id: node.id,
186 label,
187 colour: node.colour,
188 category: node.category,
189 pe: node.pe,
190 iram_offset: node.iram_offset,
191 ctx: node.ctx,
192 synthetic: node.synthetic ?? false,
193 sm_id: (node as any).sm_id ?? null,
194 },
195 classes:
196 [isSeedConst ? "seed-const" : undefined].filter(Boolean).join(" ") || undefined,
197 };
198
199 // Synthetic SM nodes and seed consts float freely
200 if (!node.synthetic && !isSeedConst && node.pe !== null) {
201 el.data.parent = `pe-${node.pe}`;
202 }
203
204 elements.push(el);
205 }
206
207 // Scan cross-PE edges to find PE pairs for bus topology
208 type PePair = { srcPe: number; tgtPe: number };
209 const pePairKey = (src: number, tgt: number) => `${src}->${tgt}`;
210 const crossPePairs = new Map<string, PePair>();
211 const busEdgeCounts = new Map<string, number>();
212
213 for (const edge of edges) {
214 if (seedConstIds.has(edge.source) || edge.synthetic) continue;
215 const srcPe = nodePeMap.get(edge.source) ?? null;
216 const tgtPe = nodePeMap.get(edge.target) ?? null;
217 if (srcPe !== null && tgtPe !== null && srcPe !== tgtPe) {
218 const key = pePairKey(srcPe, tgtPe);
219 if (!crossPePairs.has(key)) {
220 crossPePairs.set(key, { srcPe, tgtPe });
221 }
222 busEdgeCounts.set(key, (busEdgeCounts.get(key) ?? 0) + 1);
223 }
224 }
225
226 // Create port nodes for each directed PE pair
227 for (const [, pair] of crossPePairs) {
228 elements.push({
229 data: {
230 id: `port-${pair.srcPe}-to-${pair.tgtPe}-exit`,
231 parent: `pe-${pair.srcPe}`,
232 },
233 classes: "port-node",
234 });
235 elements.push({
236 data: {
237 id: `port-${pair.srcPe}-to-${pair.tgtPe}-entry`,
238 parent: `pe-${pair.tgtPe}`,
239 },
240 classes: "port-node",
241 });
242 }
243
244 // Create bus edges (one per PE pair)
245 for (const [key, pair] of crossPePairs) {
246 const count = busEdgeCounts.get(key) ?? 1;
247 elements.push({
248 data: {
249 id: `bus-${pair.srcPe}-to-${pair.tgtPe}`,
250 source: `port-${pair.srcPe}-to-${pair.tgtPe}-exit`,
251 target: `port-${pair.srcPe}-to-${pair.tgtPe}-entry`,
252 label: count > 1 ? `×${count}` : "",
253 },
254 classes: "physical bus-segment",
255 });
256 }
257
258 // Create edges with proper splitting
259 for (const edge of edges) {
260 const srcPe = nodePeMap.get(edge.source) ?? null;
261 const tgtPe = nodePeMap.get(edge.target) ?? null;
262
263 if (edge.synthetic) {
264 // Synthetic edges (SM request/return) — never bundled
265 elements.push({
266 data: {
267 id: `${edge.source}->${edge.target}:${edge.port}`,
268 source: edge.source,
269 target: edge.target,
270 targetLabel: edge.port,
271 sourceLabel: "",
272 },
273 classes: "physical synthetic",
274 });
275 } else if (seedConstIds.has(edge.source)) {
276 // Seed const: direct edge, never bundled
277 elements.push({
278 data: {
279 id: `${edge.source}->${edge.target}:${edge.port}`,
280 source: edge.source,
281 target: edge.target,
282 targetLabel: edge.port,
283 sourceLabel: "",
284 },
285 classes: "seed-edge physical",
286 });
287 } else if (srcPe !== null && tgtPe !== null && srcPe !== tgtPe) {
288 // Cross-PE: split into exit segment + entry segment (bus already created)
289 elements.push({
290 data: {
291 id: `${edge.source}->exit-${srcPe}-to-${tgtPe}:${edge.port}`,
292 source: edge.source,
293 target: `port-${srcPe}-to-${tgtPe}-exit`,
294 sourceLabel: "",
295 targetLabel: "",
296 },
297 classes: "physical exit-segment",
298 });
299 elements.push({
300 data: {
301 id: `entry-${srcPe}-to-${tgtPe}->${edge.target}:${edge.port}`,
302 source: `port-${srcPe}-to-${tgtPe}-entry`,
303 target: edge.target,
304 targetLabel: edge.port,
305 },
306 classes: "physical entry-segment",
307 });
308 } else {
309 // Intra-PE or unplaced
310 const sourceNode = nodes.find((n) => n.id === edge.source);
311 let sourceLabel: string | undefined;
312 if (sourceNode && sourceNode.category === ROUTING_CATEGORY && edge.source_port) {
313 sourceLabel = edge.source_port === "L" ? "T" : "F";
314 }
315
316 elements.push({
317 data: {
318 id: `${edge.source}->${edge.target}:${edge.port}`,
319 source: edge.source,
320 target: edge.target,
321 targetLabel: edge.port,
322 sourceLabel: sourceLabel ?? "",
323 },
324 classes: "physical intra-pe",
325 });
326 }
327 }
328
329 return elements;
330}
331
332/**
333 * Initialize Cytoscape on the graph container.
334 */
335function initializeCytoscape(): void {
336 const container = document.getElementById("graph");
337 if (!container) {
338 console.error("Graph container not found");
339 return;
340 }
341
342 cy = cytoscape({
343 container,
344 style: [...monitorStylesheet],
345 elements: [],
346 maxZoom: 2.0,
347 minZoom: 0.1,
348 });
349}
350
351/**
352 * Render graph from loaded data.
353 */
354function renderGraph(
355 nodes: ReadonlyArray<MonitorNode>,
356 edges: ReadonlyArray<MonitorEdge>,
357): void {
358 currentNodes = nodes;
359 currentEdges = edges;
360
361 const elements = isPhysicalLayout
362 ? buildPhysicalElements(nodes, edges)
363 : buildLogicalElements(nodes, edges);
364
365 cy.batch(() => {
366 cy.elements().remove();
367 cy.add(elements);
368 });
369
370 const layoutOptions = isPhysicalLayout
371 ? physicalLayout()
372 : logicalLayout();
373
374 const layout = cy.layout(layoutOptions);
375 layout.on("layoutstop", () => {
376 if (isPhysicalLayout) {
377 routeEdges(".intra-pe, .seed-edge");
378 } else {
379 routeEdges();
380 }
381 cy.fit(undefined, 40);
382 });
383 layout.run();
384}
385
386/**
387 * Update graph execution state from monitor update message.
388 */
389function updateGraphExecution(
390 nodes: ReadonlyArray<MonitorNode>,
391 edges: ReadonlyArray<MonitorEdge>,
392 state: SystemState,
393): void {
394 // Clear previous execution classes
395 cy.nodes().removeClass(["active", "executed", "matched", "half-matched", "cell-written"]);
396 cy.edges().removeClass("token-flow");
397
398 // Apply execution state to nodes
399 for (const node of nodes) {
400 const cyNode = cy.getElementById(node.id);
401 if (!cyNode || cyNode.empty()) {
402 if (node.synthetic) {
403 console.warn("[Monitor] SM node not found in cy:", node.id, "label:", node.label);
404 }
405 continue;
406 }
407
408 if (node.active) cyNode.addClass("active");
409 if (node.matched) cyNode.addClass("matched");
410 if (node.executed) cyNode.addClass("executed");
411 if (node.cell_written) cyNode.addClass("cell-written");
412
413 // Update SM node labels with live state
414 if (node.synthetic) {
415 if (node.label) {
416 cyNode.data("label", node.label);
417 }
418 }
419
420 // Check for half-matched: look in matching store
421 if (node.pe !== null && node.iram_offset !== null) {
422 const peId = node.pe.toString();
423 const peState = state.pes?.[peId];
424 if (peState && node.ctx !== null) {
425 const ctx = node.ctx;
426 const ms = peState.matching_store?.[ctx];
427 if (ms && node.iram_offset < ms.length && ms[node.iram_offset]?.occupied) {
428 cyNode.addClass("half-matched");
429 }
430 }
431 }
432 }
433
434 // Apply token flow to edges — need to handle both edge ID formats
435 for (const monitorEdge of edges) {
436 if (monitorEdge.token_flow) {
437 // Try both ID formats (logical uses source->target, physical uses source->target:port)
438 const logicalId = `${monitorEdge.source}->${monitorEdge.target}`;
439 const physicalId = `${monitorEdge.source}->${monitorEdge.target}:${monitorEdge.port}`;
440 const cyEdge = cy.getElementById(physicalId);
441 if (cyEdge && !cyEdge.empty()) {
442 cyEdge.addClass("token-flow");
443 } else {
444 const fallback = cy.getElementById(logicalId);
445 if (fallback && !fallback.empty()) {
446 fallback.addClass("token-flow");
447 }
448 }
449 }
450 }
451}
452
453/**
454 * Handle graph_loaded message from server.
455 */
456function handleGraphLoaded(msg: GraphLoadedMessage): void {
457 console.log("[Monitor] Graph loaded");
458 currentState = msg.state;
459
460 renderGraph(msg.graph.nodes, msg.graph.edges);
461
462 // Initialize panels — clear event log for fresh program
463 clearEventLog();
464 updateEventLog([], {
465 container: document.getElementById("event-log") || document.createElement("div"),
466 maxEvents: 1000,
467 });
468
469 updateStateInspector(msg.state, {
470 container: document.getElementById("state-inspector") || document.createElement("div"),
471 });
472
473 // Update sim time
474 updateSimTime(msg.sim_time, false);
475}
476
477/**
478 * Handle monitor_update message from server.
479 */
480function handleMonitorUpdate(msg: MonitorUpdate): void {
481 currentState = msg.state;
482
483 // Update graph execution state
484 updateGraphExecution(msg.graph.nodes, msg.graph.edges, msg.state);
485
486 // Update panels
487 const eventLogContainer = document.getElementById("event-log");
488 if (eventLogContainer) {
489 updateEventLog(msg.events, {
490 container: eventLogContainer,
491 maxEvents: 1000,
492 });
493 }
494
495 const stateInspectorContainer = document.getElementById("state-inspector");
496 if (stateInspectorContainer) {
497 updateStateInspector(msg.state, {
498 container: stateInspectorContainer,
499 });
500 }
501
502 updateSimTime(msg.sim_time, msg.finished);
503}
504
505/**
506 * Handle reset message from server.
507 */
508function handleReset(msg: ResetMessage): void {
509 console.log("[Monitor] Simulation reset");
510
511 // Clear the graph
512 cy.elements().remove();
513
514 // Reset event log
515 clearEventLog();
516 const eventLogContainer = document.getElementById("event-log");
517 if (eventLogContainer) {
518 updateEventLog([], {
519 container: eventLogContainer,
520 maxEvents: 1000,
521 });
522 }
523
524 // Clear state inspector
525 const stateInspectorContainer = document.getElementById("state-inspector");
526 if (stateInspectorContainer) {
527 stateInspectorContainer.innerHTML = "";
528 }
529
530 // Update simulation time
531 updateSimTime(msg.sim_time, false);
532}
533
534/**
535 * Update simulation time display.
536 */
537function updateSimTime(time: number, finished: boolean): void {
538 const timeDisplay = document.getElementById("sim-time");
539 if (!timeDisplay) return;
540
541 timeDisplay.textContent = `t=${time.toFixed(3)}`;
542
543 const finishedBadge = document.getElementById("finished-badge");
544 if (finishedBadge) {
545 finishedBadge.style.display = finished ? "inline" : "none";
546 }
547}
548
549/**
550 * Handle layout toggle.
551 */
552function toggleLayout(): void {
553 isPhysicalLayout = !isPhysicalLayout;
554 const button = document.getElementById("layout-toggle");
555 if (button) {
556 button.textContent = isPhysicalLayout ? "Logical Layout" : "Physical Layout";
557 }
558
559 // Re-render graph with stored data
560 if (currentNodes.length > 0) {
561 renderGraph(currentNodes, currentEdges);
562 }
563}
564
565/**
566 * Handle node click to show matching store (AC3.11).
567 */
568function setupNodeClickHandler(): void {
569 cy.on("tap", "node", (event: any) => {
570 const node = event.target;
571 const nodeData = node.data();
572
573 // Highlight this node
574 cy.elements().removeClass("highlighted");
575 node.addClass("highlighted");
576
577 const stateInspectorContainer = document.getElementById("state-inspector");
578 if (!currentState || !stateInspectorContainer) return;
579
580 // SM node clicked: show SM state detail
581 if (nodeData.synthetic && nodeData.sm_id !== null) {
582 const smIdStr = nodeData.sm_id.toString();
583 const smState = currentState.sms?.[smIdStr];
584 if (smState) {
585 displaySMNodeState(nodeData.sm_id, smState, stateInspectorContainer);
586 }
587 return;
588 }
589
590 // PE node clicked: show matching store info (AC3.11)
591 if (nodeData.pe !== null && nodeData.ctx !== null && nodeData.iram_offset !== null) {
592 const peId = nodeData.pe;
593 const peIdStr = peId.toString();
594 const peState = currentState.pes?.[peIdStr];
595 if (peState) {
596 const ms = peState.matching_store?.[nodeData.ctx];
597 if (ms && nodeData.iram_offset < ms.length) {
598 const entry = ms[nodeData.iram_offset];
599 displayNodeMatchingStore(peId, nodeData.ctx, nodeData.iram_offset, entry, stateInspectorContainer);
600 }
601 }
602 }
603 });
604}
605
606/**
607 * Setup control handlers.
608 */
609function setupControlHandlers(connection: MonitorConnection): void {
610 // Load file button
611 const fileInput = document.getElementById("file-input") as HTMLInputElement;
612 const loadButton = document.getElementById("load-button");
613 if (loadButton) {
614 loadButton.addEventListener("click", () => {
615 fileInput?.click();
616 });
617 }
618
619 if (fileInput) {
620 fileInput.addEventListener("change", (e) => {
621 const file = (e.target as HTMLInputElement).files?.[0];
622 if (file) {
623 const reader = new FileReader();
624 reader.onload = (event) => {
625 const source = event.target?.result as string;
626 connection.send({ cmd: "load", source });
627 };
628 reader.readAsText(file);
629 }
630 });
631 }
632
633 // Step tick button
634 const stepTickButton = document.getElementById("step-tick-button");
635 if (stepTickButton) {
636 stepTickButton.addEventListener("click", () => {
637 connection.send({ cmd: "step_tick" });
638 });
639 }
640
641 // Step event button
642 const stepEventButton = document.getElementById("step-event-button");
643 if (stepEventButton) {
644 stepEventButton.addEventListener("click", () => {
645 connection.send({ cmd: "step_event" });
646 });
647 }
648
649 // Run until button
650 const runUntilButton = document.getElementById("run-until-button");
651 const runUntilInput = document.getElementById("run-until-input") as HTMLInputElement;
652 if (runUntilButton) {
653 runUntilButton.addEventListener("click", () => {
654 const until = parseFloat(runUntilInput?.value || "0");
655 if (!isNaN(until)) {
656 connection.send({ cmd: "run_until", until });
657 }
658 });
659 }
660
661 // Send token button
662 const sendButton = document.getElementById("send-button");
663 if (sendButton) {
664 sendButton.addEventListener("click", () => {
665 const target = parseInt(
666 (document.getElementById("send-target") as HTMLInputElement)?.value || "0"
667 );
668 const offset = parseInt(
669 (document.getElementById("send-offset") as HTMLInputElement)?.value || "0"
670 );
671 const ctx = parseInt(
672 (document.getElementById("send-ctx") as HTMLInputElement)?.value || "0"
673 );
674 const data = parseInt(
675 (document.getElementById("send-data") as HTMLInputElement)?.value || "0"
676 );
677
678 if (!isNaN(target) && !isNaN(offset) && !isNaN(ctx) && !isNaN(data)) {
679 connection.send({ cmd: "send", target, offset, ctx, data });
680 }
681 });
682 }
683
684 // Inject token button
685 const injectButton = document.getElementById("inject-button");
686 if (injectButton) {
687 injectButton.addEventListener("click", () => {
688 const target = parseInt(
689 (document.getElementById("inject-target") as HTMLInputElement)?.value || "0"
690 );
691 const offset = parseInt(
692 (document.getElementById("inject-offset") as HTMLInputElement)?.value || "0"
693 );
694 const ctx = parseInt(
695 (document.getElementById("inject-ctx") as HTMLInputElement)?.value || "0"
696 );
697 const data = parseInt(
698 (document.getElementById("inject-data") as HTMLInputElement)?.value || "0"
699 );
700
701 if (!isNaN(target) && !isNaN(offset) && !isNaN(ctx) && !isNaN(data)) {
702 connection.send({ cmd: "inject", target, offset, ctx, data });
703 }
704 });
705 }
706
707 // Reset button
708 const resetButton = document.getElementById("reset-button");
709 if (resetButton) {
710 resetButton.addEventListener("click", () => {
711 connection.send({ cmd: "reset", reload: true });
712 });
713 }
714
715 // Layout toggle
716 const layoutToggle = document.getElementById("layout-toggle");
717 if (layoutToggle) {
718 layoutToggle.addEventListener("click", toggleLayout);
719 }
720
721 // Export buttons
722 const exportSvgButton = document.getElementById("export-svg-button");
723 if (exportSvgButton) {
724 exportSvgButton.addEventListener("click", () => {
725 exportSvg(cy);
726 });
727 }
728
729 const exportPngButton = document.getElementById("export-png-button");
730 if (exportPngButton) {
731 exportPngButton.addEventListener("click", async () => {
732 try {
733 await exportPng(cy);
734 } catch (err) {
735 console.error("Export PNG failed:", err);
736 }
737 });
738 }
739
740 const copyPngButton = document.getElementById("copy-png-button");
741 if (copyPngButton) {
742 copyPngButton.addEventListener("click", async () => {
743 try {
744 await copyPng(cy);
745 } catch (err) {
746 console.error("Copy PNG failed:", err);
747 }
748 });
749 }
750
751 // Event filter
752 const filterComponentSelect = document.getElementById("filter-component") as HTMLSelectElement;
753 const filterTypeSelect = document.getElementById("filter-type") as HTMLSelectElement;
754
755 function updateFilter(): void {
756 const logContainer = document.getElementById("event-log");
757 setEventFilter(
758 {
759 component: filterComponentSelect?.value || null,
760 eventType: filterTypeSelect?.value || null,
761 },
762 logContainer
763 );
764 }
765
766 if (filterComponentSelect) {
767 filterComponentSelect.addEventListener("change", updateFilter);
768 }
769 if (filterTypeSelect) {
770 filterTypeSelect.addEventListener("change", updateFilter);
771 }
772}
773
774/**
775 * Main initialization.
776 */
777function main(): void {
778 initializeCytoscape();
779 setupNodeClickHandler();
780
781 // Determine WebSocket URL
782 const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
783 const host = window.location.host;
784 const wsUrl = `${protocol}//${host}/ws`;
785
786 console.log("[Monitor] Connecting to", wsUrl);
787
788 const connection = createConnection({
789 url: wsUrl,
790 onMessage: (message: ServerMessage) => {
791 if (message.type === "graph_loaded") {
792 handleGraphLoaded(message);
793 } else if (message.type === "monitor_update") {
794 handleMonitorUpdate(message);
795 } else if (message.type === "reset") {
796 handleReset(message);
797 } else if (message.type === "error") {
798 console.error("[Monitor] Server error:", message.message);
799 }
800 },
801 onConnect: () => {
802 console.log("[Monitor] Connected");
803 const statusEl = document.getElementById("connection-status");
804 if (statusEl) {
805 statusEl.textContent = "Connected";
806 statusEl.style.color = rp.foam;
807 }
808 },
809 onDisconnect: () => {
810 console.log("[Monitor] Disconnected");
811 const statusEl = document.getElementById("connection-status");
812 if (statusEl) {
813 statusEl.textContent = "Disconnected";
814 statusEl.style.color = rp.love;
815 }
816 },
817 });
818
819 setupControlHandlers(connection);
820}
821
822// Start on DOM ready
823if (document.readyState === "loading") {
824 document.addEventListener("DOMContentLoaded", main);
825} else {
826 main();
827}