@takodotid/circuit 0.34.3 → 0.35.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 (240) hide show
  1. package/README.md +13 -14
  2. package/dist/chunks/index-20kcjxjg.js +174 -0
  3. package/dist/chunks/index-2r4bbhac.js +977 -0
  4. package/dist/chunks/index-n7w3tg5y.js +149 -0
  5. package/dist/chunks/new-w3zys5y4.js +172 -0
  6. package/dist/chunks/server-gzsf5me5.js +259 -0
  7. package/dist/cli/index.js +11345 -9024
  8. package/dist/helpers/index.js +594 -0
  9. package/dist/index.js +9 -28
  10. package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
  11. package/dist/types/core/address.d.ts +44 -0
  12. package/dist/types/core/cabling.d.ts +41 -0
  13. package/dist/types/core/cross-checks.d.ts +28 -0
  14. package/dist/types/core/define.d.ts +25 -3
  15. package/dist/types/core/project-files.d.ts +12 -0
  16. package/dist/types/core/schema-docs.d.ts +13 -4
  17. package/dist/types/core/secrets.d.ts +62 -21
  18. package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
  19. package/dist/types/core/utils.d.ts +8 -0
  20. package/dist/types/core/validator.d.ts +19 -0
  21. package/dist/types/helpers/bgp.d.ts +97 -0
  22. package/dist/types/helpers/checks.d.ts +32 -0
  23. package/dist/types/helpers/exchange.d.ts +49 -0
  24. package/dist/types/helpers/filters.d.ts +51 -0
  25. package/dist/types/helpers/index.d.ts +10 -0
  26. package/dist/types/helpers/route-server.d.ts +52 -0
  27. package/dist/types/index.d.ts +5 -3
  28. package/dist/types/integrations/librenms.d.ts +60 -0
  29. package/dist/types/integrations/netbox.d.ts +89 -0
  30. package/dist/types/integrations/peeringdb.d.ts +24 -0
  31. package/dist/types/integrations/prometheus.d.ts +24 -0
  32. package/dist/types/integrations/registries.d.ts +8 -0
  33. package/dist/types/platforms/bgp-wait.d.ts +18 -0
  34. package/dist/types/platforms/catalog.d.ts +109 -0
  35. package/dist/types/platforms/derive.d.ts +17 -0
  36. package/dist/types/platforms/fortios/firewall.d.ts +45 -0
  37. package/dist/types/platforms/fortios/index.d.ts +134 -0
  38. package/dist/types/platforms/fortios/names.d.ts +52 -0
  39. package/dist/types/platforms/fortios/plan.d.ts +104 -0
  40. package/dist/types/platforms/fortios/render.d.ts +75 -0
  41. package/dist/types/platforms/fortios/routing.d.ts +56 -0
  42. package/dist/types/platforms/fortios/tree.d.ts +37 -0
  43. package/dist/types/platforms/index.d.ts +13 -0
  44. package/dist/types/platforms/junos/bgp.d.ts +44 -0
  45. package/dist/types/platforms/junos/index.d.ts +114 -0
  46. package/dist/types/platforms/junos/render.d.ts +154 -0
  47. package/dist/types/platforms/line-diff.d.ts +67 -0
  48. package/dist/types/platforms/platform.d.ts +120 -0
  49. package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
  50. package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
  51. package/dist/types/platforms/routeros/export.d.ts +59 -0
  52. package/dist/types/platforms/routeros/index.d.ts +96 -0
  53. package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
  54. package/dist/types/platforms/routeros/plan.d.ts +54 -0
  55. package/dist/types/platforms/routeros/render.d.ts +240 -0
  56. package/dist/types/platforms/vrp/index.d.ts +83 -0
  57. package/dist/types/platforms/vrp/render.d.ts +96 -0
  58. package/dist/types/schema/common.d.ts +12 -6
  59. package/dist/types/schema/index.d.ts +1 -0
  60. package/dist/types/schema/interfaces.d.ts +14 -6
  61. package/dist/types/schema/policy.d.ts +18 -10
  62. package/dist/types/schema/routing.d.ts +14 -1
  63. package/dist/types/schema/system.d.ts +16 -6
  64. package/dist/types/schema/time-zones.d.ts +2 -0
  65. package/dist/types/transport/console.d.ts +79 -23
  66. package/dist/types/transport/path-check.d.ts +32 -0
  67. package/dist/types/transport/ssh.d.ts +17 -15
  68. package/package.json +17 -11
  69. package/src/cli/graph-format.ts +91 -0
  70. package/src/cli/index.ts +1074 -484
  71. package/src/cli/new.ts +208 -175
  72. package/src/cli/rename.ts +97 -91
  73. package/src/core/address-space.ts +320 -0
  74. package/src/core/address.ts +202 -0
  75. package/src/core/cabling.ts +154 -0
  76. package/src/core/cross-checks.ts +364 -0
  77. package/src/core/define.ts +26 -3
  78. package/src/core/project-files.ts +35 -0
  79. package/src/core/schema-docs.ts +138 -130
  80. package/src/core/secrets.ts +170 -76
  81. package/src/core/topology.ts +656 -0
  82. package/src/core/utils.ts +37 -0
  83. package/src/core/validator.ts +407 -0
  84. package/src/helpers/bgp.ts +301 -0
  85. package/src/helpers/checks.ts +178 -0
  86. package/src/helpers/exchange.ts +117 -0
  87. package/src/helpers/filters.ts +107 -0
  88. package/src/helpers/index.ts +12 -0
  89. package/src/helpers/route-server.ts +212 -0
  90. package/src/index.ts +6 -3
  91. package/src/integrations/librenms.ts +291 -0
  92. package/src/integrations/netbox.ts +620 -0
  93. package/src/integrations/peeringdb.ts +173 -0
  94. package/src/integrations/prometheus.ts +90 -0
  95. package/src/integrations/registries.ts +25 -0
  96. package/src/platforms/bgp-wait.ts +41 -0
  97. package/src/platforms/catalog.ts +69 -0
  98. package/src/platforms/derive.ts +117 -0
  99. package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
  100. package/src/platforms/fortios/firewall.ts +377 -0
  101. package/src/platforms/fortios/index.ts +1060 -0
  102. package/src/platforms/fortios/names.ts +130 -0
  103. package/src/platforms/fortios/plan.ts +929 -0
  104. package/src/platforms/fortios/render.ts +779 -0
  105. package/src/platforms/fortios/routing.ts +476 -0
  106. package/src/platforms/fortios/tree.ts +129 -0
  107. package/src/platforms/index.ts +36 -0
  108. package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
  109. package/src/platforms/junos/bgp.ts +293 -0
  110. package/src/platforms/junos/index.ts +1006 -0
  111. package/src/platforms/junos/render.ts +967 -0
  112. package/src/platforms/line-diff.ts +244 -0
  113. package/src/platforms/platform.ts +122 -0
  114. package/src/platforms/raisecom-ros/TRAPS.md +40 -0
  115. package/src/platforms/raisecom-ros/index.ts +731 -0
  116. package/src/platforms/raisecom-ros/render.ts +328 -0
  117. package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
  118. package/src/platforms/routeros/export.ts +342 -0
  119. package/src/platforms/routeros/index.ts +636 -0
  120. package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
  121. package/src/platforms/routeros/plan.ts +444 -0
  122. package/src/platforms/routeros/render.ts +1483 -0
  123. package/src/platforms/vrp/TRAPS.md +45 -0
  124. package/src/platforms/vrp/index.ts +652 -0
  125. package/src/platforms/vrp/render.ts +615 -0
  126. package/src/schema/common.ts +15 -6
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/interfaces.ts +15 -5
  129. package/src/schema/policy.ts +22 -10
  130. package/src/schema/routing.ts +16 -1
  131. package/src/schema/system.ts +16 -6
  132. package/src/schema/time-zones.ts +482 -0
  133. package/src/studio/data.ts +172 -168
  134. package/src/studio/git.ts +95 -90
  135. package/src/studio/public/app.js +161 -150
  136. package/src/studio/public/compare.js +219 -212
  137. package/src/studio/public/diff.js +77 -75
  138. package/src/studio/public/dom.js +118 -108
  139. package/src/studio/public/evaluate.js +584 -545
  140. package/src/studio/public/export.js +120 -118
  141. package/src/studio/public/graph.js +1343 -1266
  142. package/src/studio/public/net.js +115 -109
  143. package/src/studio/public/tables.js +1135 -1083
  144. package/src/studio/public/tools.js +561 -529
  145. package/src/studio/server.ts +202 -196
  146. package/src/transport/console.ts +363 -271
  147. package/src/transport/path-check.ts +91 -0
  148. package/src/transport/ssh.ts +98 -94
  149. package/templates/colocation/checks.ts +11 -6
  150. package/templates/colocation/edge-01.ts +9 -9
  151. package/templates/colocation/routing.ts +2 -2
  152. package/templates/colocation/tenancy.ts +3 -3
  153. package/templates/colocation/tor-01.ts +3 -3
  154. package/templates/edge-router/checks.ts +4 -4
  155. package/templates/edge-router/edge-01.ts +5 -5
  156. package/templates/edge-router/routing.ts +2 -2
  157. package/templates/project/AGENTS.md +5 -1
  158. package/templates/single-site/checks.ts +2 -2
  159. package/dist/chunks/index-47debt16.js +0 -956
  160. package/dist/chunks/index-v8zgr7wm.js +0 -128
  161. package/dist/chunks/new-ny89s7y7.js +0 -166
  162. package/dist/chunks/server-tjbb3rqc.js +0 -251
  163. package/dist/presets/index.js +0 -394
  164. package/dist/types/adapters/devices/catalog.d.ts +0 -86
  165. package/dist/types/adapters/devices/derive.d.ts +0 -7
  166. package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
  167. package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
  168. package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
  169. package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
  170. package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
  171. package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
  172. package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
  173. package/dist/types/adapters/devices/index.d.ts +0 -3
  174. package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
  175. package/dist/types/adapters/devices/junos/index.d.ts +0 -42
  176. package/dist/types/adapters/devices/junos/render.d.ts +0 -52
  177. package/dist/types/adapters/devices/lines.d.ts +0 -56
  178. package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
  179. package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
  180. package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
  181. package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
  182. package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
  183. package/dist/types/adapters/devices/types.d.ts +0 -40
  184. package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
  185. package/dist/types/adapters/registries/index.d.ts +0 -3
  186. package/dist/types/adapters/registries/librenms.d.ts +0 -15
  187. package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
  188. package/dist/types/adapters/registries/prometheus.d.ts +0 -16
  189. package/dist/types/core/across.d.ts +0 -4
  190. package/dist/types/core/addr.d.ts +0 -34
  191. package/dist/types/core/files.d.ts +0 -6
  192. package/dist/types/core/merge.d.ts +0 -4
  193. package/dist/types/core/time.d.ts +0 -2
  194. package/dist/types/core/validate.d.ts +0 -2
  195. package/dist/types/presets/index.d.ts +0 -183
  196. package/src/adapters/devices/catalog.ts +0 -41
  197. package/src/adapters/devices/derive.ts +0 -59
  198. package/src/adapters/devices/fortios/firewall.ts +0 -362
  199. package/src/adapters/devices/fortios/index.ts +0 -923
  200. package/src/adapters/devices/fortios/names.ts +0 -115
  201. package/src/adapters/devices/fortios/plan.ts +0 -888
  202. package/src/adapters/devices/fortios/render.ts +0 -768
  203. package/src/adapters/devices/fortios/routing.ts +0 -444
  204. package/src/adapters/devices/fortios/tree.ts +0 -121
  205. package/src/adapters/devices/index.ts +0 -9
  206. package/src/adapters/devices/junos/bgp.ts +0 -269
  207. package/src/adapters/devices/junos/index.ts +0 -862
  208. package/src/adapters/devices/junos/render.ts +0 -872
  209. package/src/adapters/devices/lines.ts +0 -212
  210. package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -700
  212. package/src/adapters/devices/routeros/export.ts +0 -321
  213. package/src/adapters/devices/routeros/index.ts +0 -346
  214. package/src/adapters/devices/routeros/plan.ts +0 -398
  215. package/src/adapters/devices/routeros/render.ts +0 -1490
  216. package/src/adapters/devices/types.ts +0 -38
  217. package/src/adapters/devices/vrp/TRAPS.md +0 -26
  218. package/src/adapters/devices/vrp/index.ts +0 -865
  219. package/src/adapters/registries/index.ts +0 -19
  220. package/src/adapters/registries/librenms.ts +0 -92
  221. package/src/adapters/registries/peeringdb.ts +0 -169
  222. package/src/adapters/registries/prometheus.ts +0 -71
  223. package/src/cli/graph.ts +0 -89
  224. package/src/core/across.ts +0 -350
  225. package/src/core/addr.ts +0 -174
  226. package/src/core/addresses.ts +0 -316
  227. package/src/core/files.ts +0 -32
  228. package/src/core/graph.ts +0 -640
  229. package/src/core/merge.ts +0 -19
  230. package/src/core/time.ts +0 -15
  231. package/src/core/validate.ts +0 -348
  232. package/src/presets/index.ts +0 -579
  233. /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
  234. /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
  235. /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
  236. /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
  237. /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
  238. /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
  239. /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
  240. /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
@@ -2,174 +2,185 @@
2
2
  /// <reference lib="dom" />
3
3
  // Studio's page: loads what the server gathered from the config, shows the place the address names, and loads again whenever the server says the config changed.
4
4
 
5
- import { byId, enter, go } from "./dom.js";
6
- import { currentLayer, isLayer, setGraph, showLayer } from "./graph.js";
7
- import { problemCount, showTables } from "./tables.js";
8
- import { showTools } from "./tools.js";
9
- import { showCompare } from "./compare.js";
5
+ import { Dom } from "./dom.js";
6
+ import { Graph } from "./graph.js";
7
+ import { Tables } from "./tables.js";
8
+ import { Tools } from "./tools.js";
9
+ import { Compare } from "./compare.js";
10
10
 
11
11
  /** @typedef {import("../data").StudioData} StudioData */
12
12
 
13
- const views = byId("views");
14
- const search = /** @type {HTMLInputElement} */ (byId("search"));
15
- const summary = byId("summary");
16
- const live = byId("live");
17
- const banner = byId("banner");
18
- const graphView = byId("graph-view");
19
- const tablesView = byId("tables-view");
20
- const tables = byId("tables");
21
- const problems = byId("problem-count");
22
-
23
- /** What the server gathered last. @type {StudioData | undefined} */
24
- let studio;
25
- /** The address last shown, so drawing the same place again keeps where the reader had scrolled to. */
26
- let shownRoute = "";
27
-
28
- for (const button of views.querySelectorAll("button")) {
29
- const view = button.dataset.view ?? "graph";
30
- button.addEventListener("click", () => go(view === "graph" ? `graph/${currentLayer()}` : view));
31
- }
32
-
33
- function showSummary() {
34
- if (!studio) return;
35
-
36
- const { graph } = studio;
37
- const devices = studio.devices.length;
38
- const errors = graph.findings.filter((finding) => finding.level === "error").length;
39
- const warnings = graph.findings.length - errors;
40
- const drifted = Object.values(graph.drift).filter((drift) => drift.status === "differs").length;
41
-
42
- const count = problemCount(studio);
43
- problems.textContent = count ? String(count) : "";
44
-
45
- const items = [
46
- { value: devices, label: devices === 1 ? "device" : "devices", state: "neutral" },
47
- { value: errors, label: errors === 1 ? "error" : "errors", state: errors ? "error" : "quiet" },
48
- { value: warnings, label: warnings === 1 ? "warning" : "warnings", state: warnings ? "warning" : "quiet" },
49
- { value: drifted, label: "unlike the snapshot", state: drifted ? "changed" : "quiet" },
50
- ];
51
-
52
- summary.replaceChildren(
53
- ...items.map(({ value, label, state }) => {
54
- const item = document.createElement("span");
55
- item.className = `summary-item ${state}`;
56
- item.append(String(value), ` ${label}`);
57
- return item;
58
- })
59
- );
60
- }
61
-
62
- /** Show the place the address names, such as `#graph/bgp` or `#devices/edge-01/ports`. */
63
- function route() {
64
- if (!studio) return;
65
-
66
- const parts = location.hash.slice(1).split("/").map(decodeURIComponent).filter(Boolean);
67
- const [view] = parts;
68
-
69
- // An address from before the tables, such as `#bgp`, still opens that layer.
70
- const isGraph = !view || view === "graph" || isLayer(view);
71
- const asked = view === "graph" ? parts[1] : view;
72
-
73
- const moved = location.hash !== shownRoute;
74
- shownRoute = location.hash;
75
-
76
- for (const button of views.querySelectorAll("button")) {
77
- const pressed = isGraph ? button.dataset.view === "graph" : button.dataset.view === view;
78
- button.setAttribute("aria-pressed", String(pressed));
13
+ export class App {
14
+ static #views = Dom.byId("views");
15
+ static #search = /** @type {HTMLInputElement} */ (Dom.byId("search"));
16
+ static #summary = Dom.byId("summary");
17
+ static #live = Dom.byId("live");
18
+ static #banner = Dom.byId("banner");
19
+ static #graphView = Dom.byId("graph-view");
20
+ static #tablesView = Dom.byId("tables-view");
21
+ static #tables = Dom.byId("tables");
22
+ static #problems = Dom.byId("problem-count");
23
+
24
+ /** What the server gathered last. @type {StudioData | undefined} */
25
+ static #studio;
26
+ /** The address last shown, so drawing the same place again keeps where the reader had scrolled to. */
27
+ static #shownRoute = "";
28
+
29
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
30
+ static #searching;
31
+
32
+ static #showSummary() {
33
+ const studio = App.#studio;
34
+ if (!studio) return;
35
+
36
+ const { graph } = studio;
37
+ const devices = studio.devices.length;
38
+ const errors = graph.findings.filter((finding) => finding.level === "error").length;
39
+ const warnings = graph.findings.length - errors;
40
+ const drifted = Object.values(graph.drift).filter((drift) => drift.status === "differs").length;
41
+
42
+ const count = Tables.problemCount(studio);
43
+ App.#problems.textContent = count ? String(count) : "";
44
+
45
+ const items = [
46
+ { value: devices, label: devices === 1 ? "device" : "devices", state: "neutral" },
47
+ { value: errors, label: errors === 1 ? "error" : "errors", state: errors ? "error" : "quiet" },
48
+ { value: warnings, label: warnings === 1 ? "warning" : "warnings", state: warnings ? "warning" : "quiet" },
49
+ { value: drifted, label: "unlike the snapshot", state: drifted ? "changed" : "quiet" },
50
+ ];
51
+
52
+ App.#summary.replaceChildren(
53
+ ...items.map(({ value, label, state }) => {
54
+ const item = document.createElement("span");
55
+ item.className = `summary-item ${state}`;
56
+ item.append(String(value), ` ${label}`);
57
+ return item;
58
+ })
59
+ );
79
60
  }
80
61
 
81
- graphView.hidden = !isGraph;
82
- tablesView.hidden = isGraph;
62
+ /** Show the place the address names, such as `#graph/bgp` or `#devices/edge-01/ports`. */
63
+ static #route() {
64
+ const studio = App.#studio;
65
+ if (!studio) return;
83
66
 
84
- if (view === "search") {
85
- if (search.value !== (parts[1] ?? "")) search.value = parts[1] ?? "";
86
- } else if (document.activeElement !== search) {
87
- search.value = "";
88
- }
67
+ const parts = location.hash.slice(1).split("/").map(decodeURIComponent).filter(Boolean);
68
+ const [view] = parts;
89
69
 
90
- showSummary();
70
+ // An address from before the tables, such as `#bgp`, still opens that layer.
71
+ const isGraph = !view || view === "graph" || Graph.isLayer(view);
72
+ const asked = view === "graph" ? parts[1] : view;
91
73
 
92
- if (isGraph) {
93
- showLayer(isLayer(asked) ? asked : currentLayer());
94
- return;
95
- }
74
+ const moved = location.hash !== App.#shownRoute;
75
+ App.#shownRoute = location.hash;
96
76
 
97
- const scrolled = tables.scrollTop;
98
- if (view === "tools") showTools(parts, tables, studio);
99
- else if (view === "compare") showCompare(parts, tables, studio);
100
- else showTables(parts, tables, studio);
101
- tables.scrollTop = moved ? 0 : scrolled;
77
+ for (const button of App.#views.querySelectorAll("button")) {
78
+ const pressed = isGraph ? button.dataset.view === "graph" : button.dataset.view === view;
79
+ button.setAttribute("aria-pressed", String(pressed));
80
+ }
102
81
 
103
- // A new place comes in; the same place drawn again, because the config changed, stays still.
104
- if (moved) enter(tables);
105
- }
82
+ App.#graphView.hidden = !isGraph;
83
+ App.#tablesView.hidden = isGraph;
106
84
 
107
- window.addEventListener("hashchange", route);
85
+ if (view === "search") {
86
+ if (App.#search.value !== (parts[1] ?? "")) App.#search.value = parts[1] ?? "";
87
+ } else if (document.activeElement !== App.#search) {
88
+ App.#search.value = "";
89
+ }
108
90
 
109
- // A menu closes once one of its items is chosen, or when the reader clicks anywhere else.
110
- document.addEventListener("click", (event) => {
111
- const target = event.target instanceof Element ? event.target : undefined;
91
+ App.#showSummary();
112
92
 
113
- for (const menu of document.querySelectorAll("details.menu[open]")) {
114
- if (!target || !menu.contains(target) || target.closest(".menu-items button")) menu.removeAttribute("open");
115
- }
116
- });
117
-
118
- /** @type {ReturnType<typeof setTimeout> | undefined} */
119
- let searching;
120
-
121
- search.addEventListener("input", () => {
122
- clearTimeout(searching);
93
+ if (isGraph) {
94
+ Graph.show(Graph.isLayer(asked) ? asked : Graph.currentLayer());
95
+ return;
96
+ }
123
97
 
124
- const query = search.value.trim();
125
- searching = setTimeout(() => go(query ? `search/${encodeURIComponent(query)}` : "devices"), 200);
126
- });
98
+ const tables = App.#tables;
99
+ const scrolled = tables.scrollTop;
100
+ if (view === "tools") Tools.show(parts, tables, studio);
101
+ else if (view === "compare") Compare.show(parts, tables, studio);
102
+ else Tables.show(parts, tables, studio);
103
+ tables.scrollTop = moved ? 0 : scrolled;
127
104
 
128
- /** Load what Studio shows, at the start and whenever the server says the config changed. */
129
- async function load() {
130
- /** @type {(StudioData & { error?: undefined }) | { error: string }} */
131
- let next;
132
-
133
- try {
134
- next = await (await fetch("data", { cache: "no-store" })).json();
135
- } catch (error) {
136
- next = { error: `could not reach circuit studio: ${error instanceof Error ? error.message : String(error)}` };
105
+ // A new place comes in; the same place drawn again, because the config changed, stays still.
106
+ if (moved) Dom.enter(tables);
137
107
  }
138
108
 
139
- if (next.error !== undefined) {
140
- banner.textContent = `The config could not be read, so what is shown is the last that could:\n${next.error}`;
141
- banner.hidden = false;
142
- return;
109
+ /** Load what Studio shows, at the start and whenever the server says the config changed. */
110
+ static async #load() {
111
+ /** @type {(StudioData & { error?: undefined }) | { error: string }} */
112
+ let next;
113
+
114
+ try {
115
+ next = await (await fetch("data", { cache: "no-store" })).json();
116
+ } catch (error) {
117
+ next = { error: `could not reach circuit studio: ${error instanceof Error ? error.message : String(error)}` };
118
+ }
119
+
120
+ if (next.error !== undefined) {
121
+ App.#banner.textContent = `The config could not be read, so what is shown is the last that could:\n${next.error}`;
122
+ App.#banner.hidden = false;
123
+ return;
124
+ }
125
+
126
+ App.#banner.hidden = true;
127
+ App.#studio = next;
128
+ Graph.set(next.graph);
129
+ App.#route();
143
130
  }
144
131
 
145
- banner.hidden = true;
146
- studio = next;
147
- setGraph(next.graph);
148
- route();
132
+ /** Wire up the page and load what it shows. */
133
+ static start() {
134
+ for (const button of App.#views.querySelectorAll("button")) {
135
+ const view = button.dataset.view ?? "graph";
136
+ button.addEventListener("click", () => Dom.go(view === "graph" ? `graph/${Graph.currentLayer()}` : view));
137
+ }
138
+
139
+ window.addEventListener("hashchange", App.#route);
140
+
141
+ // A menu closes once one of its items is chosen, or when the reader clicks anywhere else.
142
+ document.addEventListener("click", (event) => {
143
+ const target = event.target instanceof Element ? event.target : undefined;
144
+
145
+ for (const menu of document.querySelectorAll("details.menu[open]")) {
146
+ if (!target || !menu.contains(target) || target.closest(".menu-items button")) menu.removeAttribute("open");
147
+ }
148
+ });
149
+
150
+ App.#search.addEventListener("input", () => {
151
+ clearTimeout(App.#searching);
152
+
153
+ const query = App.#search.value.trim();
154
+ App.#searching = setTimeout(() => Dom.go(query ? `search/${encodeURIComponent(query)}` : "devices"), 200);
155
+ });
156
+
157
+ const events = new EventSource("events");
158
+ const live = App.#live;
159
+
160
+ events.addEventListener("open", () => {
161
+ live.textContent = "live";
162
+ live.title = "Watching the config: a saved change shows here at once.";
163
+ live.classList.add("on");
164
+ });
165
+
166
+ events.addEventListener("error", () => {
167
+ live.textContent = "not connected";
168
+ live.title = "Circuit Studio is not answering. Is circuit studio still running?";
169
+ live.classList.remove("on");
170
+ });
171
+
172
+ // The config changed: load it again, and the live mark beats once to say so. The page itself changed, while working on Circuit: load the page again.
173
+ events.addEventListener("message", (event) => {
174
+ if (event.data === "page") return location.reload();
175
+
176
+ live.classList.remove("beat");
177
+ void live.offsetWidth;
178
+ live.classList.add("beat");
179
+ void App.#load();
180
+ });
181
+
182
+ void App.#load();
183
+ }
149
184
  }
150
185
 
151
- const events = new EventSource("events");
152
-
153
- events.addEventListener("open", () => {
154
- live.textContent = "live";
155
- live.title = "Watching the config: a saved change shows here at once.";
156
- live.classList.add("on");
157
- });
158
-
159
- events.addEventListener("error", () => {
160
- live.textContent = "not connected";
161
- live.title = "Circuit Studio is not answering. Is circuit studio still running?";
162
- live.classList.remove("on");
163
- });
164
-
165
- // The config changed: load it again, and the live mark beats once to say so. The page itself changed, while working on Circuit: load the page again.
166
- events.addEventListener("message", (event) => {
167
- if (event.data === "page") return location.reload();
168
-
169
- live.classList.remove("beat");
170
- void live.offsetWidth;
171
- live.classList.add("beat");
172
- void load();
173
- });
174
-
175
- void load();
186
+ App.start();