blokctl 0.2.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 (169) hide show
  1. package/dist/commands/build/index.d.ts +2 -0
  2. package/dist/commands/build/index.js +210 -0
  3. package/dist/commands/config/index.d.ts +1 -0
  4. package/dist/commands/config/index.js +46 -0
  5. package/dist/commands/cost/index.d.ts +1 -0
  6. package/dist/commands/cost/index.js +74 -0
  7. package/dist/commands/create/node.d.ts +2 -0
  8. package/dist/commands/create/node.js +541 -0
  9. package/dist/commands/create/project.d.ts +2 -0
  10. package/dist/commands/create/project.js +941 -0
  11. package/dist/commands/create/utils/Examples.d.ts +39 -0
  12. package/dist/commands/create/utils/Examples.js +983 -0
  13. package/dist/commands/create/workflow.d.ts +2 -0
  14. package/dist/commands/create/workflow.js +109 -0
  15. package/dist/commands/deploy/index.d.ts +2 -0
  16. package/dist/commands/deploy/index.js +176 -0
  17. package/dist/commands/dev/index.d.ts +2 -0
  18. package/dist/commands/dev/index.js +190 -0
  19. package/dist/commands/generate/GenerationAnalytics.d.ts +61 -0
  20. package/dist/commands/generate/GenerationAnalytics.js +162 -0
  21. package/dist/commands/generate/GenerationAnalytics.test.d.ts +1 -0
  22. package/dist/commands/generate/GenerationAnalytics.test.js +407 -0
  23. package/dist/commands/generate/NodeFileWriter.d.ts +5 -0
  24. package/dist/commands/generate/NodeFileWriter.js +240 -0
  25. package/dist/commands/generate/NodeGenerator.d.ts +20 -0
  26. package/dist/commands/generate/NodeGenerator.js +181 -0
  27. package/dist/commands/generate/NodeGenerator.test.d.ts +1 -0
  28. package/dist/commands/generate/NodeGenerator.test.js +101 -0
  29. package/dist/commands/generate/PromptVersioning.d.ts +25 -0
  30. package/dist/commands/generate/PromptVersioning.js +71 -0
  31. package/dist/commands/generate/PromptVersioning.test.d.ts +1 -0
  32. package/dist/commands/generate/PromptVersioning.test.js +120 -0
  33. package/dist/commands/generate/RegisterNode.d.ts +3 -0
  34. package/dist/commands/generate/RegisterNode.js +37 -0
  35. package/dist/commands/generate/RuntimeGenerator.d.ts +40 -0
  36. package/dist/commands/generate/RuntimeGenerator.js +369 -0
  37. package/dist/commands/generate/RuntimeGenerator.test.d.ts +1 -0
  38. package/dist/commands/generate/RuntimeGenerator.test.js +553 -0
  39. package/dist/commands/generate/TriggerGenerator.d.ts +22 -0
  40. package/dist/commands/generate/TriggerGenerator.js +220 -0
  41. package/dist/commands/generate/TriggerGenerator.test.d.ts +1 -0
  42. package/dist/commands/generate/TriggerGenerator.test.js +209 -0
  43. package/dist/commands/generate/WorkflowGenerator.d.ts +20 -0
  44. package/dist/commands/generate/WorkflowGenerator.js +131 -0
  45. package/dist/commands/generate/WorkflowGenerator.test.d.ts +1 -0
  46. package/dist/commands/generate/WorkflowGenerator.test.js +77 -0
  47. package/dist/commands/generate/e2e/NodeGenerator.e2e.test.d.ts +1 -0
  48. package/dist/commands/generate/e2e/NodeGenerator.e2e.test.js +216 -0
  49. package/dist/commands/generate/e2e/RuntimeGenerator.e2e.test.d.ts +1 -0
  50. package/dist/commands/generate/e2e/RuntimeGenerator.e2e.test.js +759 -0
  51. package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.d.ts +1 -0
  52. package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.js +295 -0
  53. package/dist/commands/generate/e2e/WorkflowGenerator.e2e.test.d.ts +1 -0
  54. package/dist/commands/generate/e2e/WorkflowGenerator.e2e.test.js +353 -0
  55. package/dist/commands/generate/index.d.ts +1 -0
  56. package/dist/commands/generate/index.js +418 -0
  57. package/dist/commands/generate/prompts/create-fn-node.system.d.ts +5 -0
  58. package/dist/commands/generate/prompts/create-fn-node.system.js +256 -0
  59. package/dist/commands/generate/prompts/create-node-manifest.system.d.ts +4 -0
  60. package/dist/commands/generate/prompts/create-node-manifest.system.js +41 -0
  61. package/dist/commands/generate/prompts/create-node.system.d.ts +5 -0
  62. package/dist/commands/generate/prompts/create-node.system.js +114 -0
  63. package/dist/commands/generate/prompts/create-readme.system.d.ts +4 -0
  64. package/dist/commands/generate/prompts/create-readme.system.js +83 -0
  65. package/dist/commands/generate/prompts/create-runtime.system.d.ts +5 -0
  66. package/dist/commands/generate/prompts/create-runtime.system.js +284 -0
  67. package/dist/commands/generate/prompts/create-trigger.system.d.ts +5 -0
  68. package/dist/commands/generate/prompts/create-trigger.system.js +293 -0
  69. package/dist/commands/generate/prompts/create-workflow.system.d.ts +5 -0
  70. package/dist/commands/generate/prompts/create-workflow.system.js +476 -0
  71. package/dist/commands/generate/prompts/register-node.system.d.ts +4 -0
  72. package/dist/commands/generate/prompts/register-node.system.js +26 -0
  73. package/dist/commands/generate/validators/CompilationValidator.d.ts +9 -0
  74. package/dist/commands/generate/validators/CompilationValidator.js +86 -0
  75. package/dist/commands/generate/validators/CompilationValidator.test.d.ts +1 -0
  76. package/dist/commands/generate/validators/CompilationValidator.test.js +161 -0
  77. package/dist/commands/generate/validators/NodeValidator.d.ts +18 -0
  78. package/dist/commands/generate/validators/NodeValidator.js +217 -0
  79. package/dist/commands/generate/validators/NodeValidator.test.d.ts +1 -0
  80. package/dist/commands/generate/validators/NodeValidator.test.js +281 -0
  81. package/dist/commands/generate/validators/WorkflowValidator.d.ts +6 -0
  82. package/dist/commands/generate/validators/WorkflowValidator.js +301 -0
  83. package/dist/commands/generate/validators/WorkflowValidator.test.d.ts +1 -0
  84. package/dist/commands/generate/validators/WorkflowValidator.test.js +647 -0
  85. package/dist/commands/generate/validators/index.d.ts +4 -0
  86. package/dist/commands/generate/validators/index.js +2 -0
  87. package/dist/commands/graph/index.d.ts +1 -0
  88. package/dist/commands/graph/index.js +69 -0
  89. package/dist/commands/install/index.d.ts +1 -0
  90. package/dist/commands/install/index.js +4 -0
  91. package/dist/commands/install/node.d.ts +4 -0
  92. package/dist/commands/install/node.js +136 -0
  93. package/dist/commands/install/workflow.d.ts +4 -0
  94. package/dist/commands/install/workflow.js +62 -0
  95. package/dist/commands/login/index.d.ts +2 -0
  96. package/dist/commands/login/index.js +77 -0
  97. package/dist/commands/logout/index.d.ts +2 -0
  98. package/dist/commands/logout/index.js +20 -0
  99. package/dist/commands/marketplace/runtime.d.ts +54 -0
  100. package/dist/commands/marketplace/runtime.js +350 -0
  101. package/dist/commands/migrate/index.d.ts +1 -0
  102. package/dist/commands/migrate/index.js +14 -0
  103. package/dist/commands/migrate/node.d.ts +2 -0
  104. package/dist/commands/migrate/node.js +110 -0
  105. package/dist/commands/monitor/index.d.ts +1 -0
  106. package/dist/commands/monitor/index.js +28 -0
  107. package/dist/commands/monitor/monitor-component.d.ts +1 -0
  108. package/dist/commands/monitor/monitor-component.js +271 -0
  109. package/dist/commands/monitor/static/index.html +2124 -0
  110. package/dist/commands/monitor/static-web-server.d.ts +1 -0
  111. package/dist/commands/monitor/static-web-server.js +89 -0
  112. package/dist/commands/profile/index.d.ts +1 -0
  113. package/dist/commands/profile/index.js +112 -0
  114. package/dist/commands/publish/index.d.ts +1 -0
  115. package/dist/commands/publish/index.js +4 -0
  116. package/dist/commands/publish/node.d.ts +4 -0
  117. package/dist/commands/publish/node.js +231 -0
  118. package/dist/commands/publish/workflow.d.ts +4 -0
  119. package/dist/commands/publish/workflow.js +165 -0
  120. package/dist/commands/search/docs.d.ts +17 -0
  121. package/dist/commands/search/docs.js +179 -0
  122. package/dist/commands/search/index.d.ts +1 -0
  123. package/dist/commands/search/index.js +5 -0
  124. package/dist/commands/search/indexer.d.ts +10 -0
  125. package/dist/commands/search/indexer.js +265 -0
  126. package/dist/commands/search/nodes.d.ts +4 -0
  127. package/dist/commands/search/nodes.js +101 -0
  128. package/dist/commands/search/workflow.d.ts +4 -0
  129. package/dist/commands/search/workflow.js +100 -0
  130. package/dist/commands/trace/index.d.ts +1 -0
  131. package/dist/commands/trace/index.js +26 -0
  132. package/dist/commands/trace/startStudio.d.ts +8 -0
  133. package/dist/commands/trace/startStudio.js +116 -0
  134. package/dist/index.d.ts +17 -0
  135. package/dist/index.js +186 -0
  136. package/dist/services/commander.d.ts +9 -0
  137. package/dist/services/commander.js +20 -0
  138. package/dist/services/constants.d.ts +1 -0
  139. package/dist/services/constants.js +3 -0
  140. package/dist/services/local-token-manager.d.ts +14 -0
  141. package/dist/services/local-token-manager.js +99 -0
  142. package/dist/services/non-interactive.d.ts +5 -0
  143. package/dist/services/non-interactive.js +30 -0
  144. package/dist/services/package-manager.d.ts +35 -0
  145. package/dist/services/package-manager.js +111 -0
  146. package/dist/services/posthog.d.ts +31 -0
  147. package/dist/services/posthog.js +159 -0
  148. package/dist/services/registry-manager.d.ts +9 -0
  149. package/dist/services/registry-manager.js +26 -0
  150. package/dist/services/runtime-detector.d.ts +23 -0
  151. package/dist/services/runtime-detector.js +181 -0
  152. package/dist/services/runtime-setup.d.ts +36 -0
  153. package/dist/services/runtime-setup.js +250 -0
  154. package/dist/services/utils.d.ts +2 -0
  155. package/dist/services/utils.js +29 -0
  156. package/dist/services/workflow-loader.d.ts +30 -0
  157. package/dist/services/workflow-loader.js +46 -0
  158. package/dist/studio-dist/assets/charts-Dso0hPUR.js +68 -0
  159. package/dist/studio-dist/assets/graph-CsV2nWGn.js +23 -0
  160. package/dist/studio-dist/assets/icons-zP8LLgPh.js +311 -0
  161. package/dist/studio-dist/assets/index-CLyEkXMx.css +1 -0
  162. package/dist/studio-dist/assets/index-CNXFX_ar.js +27 -0
  163. package/dist/studio-dist/assets/react-vendor--Eh9ivFN.js +17 -0
  164. package/dist/studio-dist/assets/tanstack-query-CiM1U6F5.js +1 -0
  165. package/dist/studio-dist/assets/tanstack-router-Btjy0MKq.js +25 -0
  166. package/dist/studio-dist/assets/tanstack-table-DhwRvuH2.js +22 -0
  167. package/dist/studio-dist/favicon.svg +5 -0
  168. package/dist/studio-dist/index.html +21 -0
  169. package/package.json +75 -0
@@ -0,0 +1,2124 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Dashboard - blok</title>
6
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
7
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3"></script>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ darkMode: "class",
12
+ };
13
+ </script>
14
+ <script src="https://unpkg.com/feather-icons"></script>
15
+ <link
16
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
17
+ rel="stylesheet"
18
+ />
19
+
20
+ <link
21
+ href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;600&display=swap"
22
+ rel="stylesheet"
23
+ />
24
+
25
+ <style>
26
+ html {
27
+ font-family: "Inter", sans-serif !important;
28
+ image-rendering: optimizeQuality;
29
+ image-resolution: inherit;
30
+ }
31
+
32
+ .metric-value,
33
+ .log-line,
34
+ .canvas-label {
35
+ font-family: "Roboto Mono", monospace !important;
36
+ }
37
+
38
+ canvas:fullscreen {
39
+ width: 100vw !important;
40
+ height: 100vh !important;
41
+ background-color: #ffffff !important;
42
+ }
43
+ </style>
44
+ </head>
45
+ <body class="bg-gray-100 text-gray-900 dark:bg-[#1d2536] dark:text-white">
46
+ <nav
47
+ class="bg-white dark:bg-gray-900 shadow-sm px-6 py-3 sticky top-0 z-50 flex items-center justify-between border-b border-gray-200 dark:border-gray-700"
48
+ >
49
+ <div class="flex flex-col">
50
+ <h1
51
+ class="text-lg font-semibold tracking-tight text-gray-900 dark:text-white"
52
+ >
53
+ Observability
54
+ </h1>
55
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
56
+ Realtime usage, latency, and error insights for all workflows and
57
+ nodes.
58
+ </p>
59
+ </div>
60
+ <div class="flex items-center justify-center gap-1">
61
+ <!-- Filters container -->
62
+ <div
63
+ class="flex items-start items-end gap-4 ml-4 md:ml-8 mt-4 md:mt-0 relative z-40 w-full flex-col xl:flex-row xl:mr-2"
64
+ >
65
+ <!-- Sections filter dropdown -->
66
+ <div class="relative flex items-center gap-2">
67
+ <label
68
+ for="section-filter"
69
+ class="text-xs text-gray-400 dark:text-blue-500 rounded px-[6px] py-[9.25px] bg-gray-100 dark:bg-gray-800 font-semibold w-[74px] text-center"
70
+ >Sections</label
71
+ >
72
+ <div class="relative">
73
+ <div
74
+ id="section-filter-trigger"
75
+ class="flex items-center justify-between border border-gray-300 dark:border-gray-600 rounded px-3 py-1.5 text-sm text-gray-800 dark:text-white w-56 cursor-pointer select-none"
76
+ >
77
+ <span id="section-filter-placeholder">All</span>
78
+ <svg
79
+ id="section-filter-chevron"
80
+ class="w-4 h-4 ml-2 transition-transform transform text-gray-500 dark:text-gray-300"
81
+ fill="none"
82
+ stroke="currentColor"
83
+ stroke-width="2"
84
+ viewBox="0 0 24 24"
85
+ >
86
+ <path
87
+ stroke-linecap="round"
88
+ stroke-linejoin="round"
89
+ d="M19 9l-7 7-7-7"
90
+ />
91
+ </svg>
92
+ </div>
93
+ <div
94
+ id="section-filter-dropdown"
95
+ class="absolute mt-1 z-50 bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded shadow-md w-full md:w-56 max-h-60 overflow-y-auto hidden"
96
+ >
97
+ <div class="px-3 py-2 text-xs text-gray-400 dark:text-gray-500">
98
+ Selected (All)
99
+ </div>
100
+ <div id="section-filter-options" class="flex flex-col"></div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ <!-- Workflows filter dropdown -->
105
+ <div class="relative flex items-center gap-2">
106
+ <label
107
+ for="workflow-filter"
108
+ class="text-xs text-gray-400 dark:text-blue-500 rounded px-[6px] py-[9.25px] bg-gray-100 dark:bg-gray-800 font-semibold w-[74px] text-center"
109
+ >Workflows</label
110
+ >
111
+ <div class="relative">
112
+ <div
113
+ id="workflow-filter-trigger"
114
+ class="flex items-center justify-between border border-gray-300 dark:border-gray-600 rounded px-3 py-1.5 text-sm text-gray-800 dark:text-white w-56 cursor-pointer select-none"
115
+ >
116
+ <span id="workflow-filter-placeholder">All</span>
117
+ <svg
118
+ id="workflow-filter-chevron"
119
+ class="w-4 h-4 ml-2 transition-transform transform text-gray-500 dark:text-gray-300"
120
+ fill="none"
121
+ stroke="currentColor"
122
+ stroke-width="2"
123
+ viewBox="0 0 24 24"
124
+ >
125
+ <path
126
+ stroke-linecap="round"
127
+ stroke-linejoin="round"
128
+ d="M19 9l-7 7-7-7"
129
+ />
130
+ </svg>
131
+ </div>
132
+ <div
133
+ id="workflow-filter-dropdown"
134
+ class="absolute mt-1 z-50 bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded shadow-md w-full md:w-56 max-h-60 overflow-y-auto hidden"
135
+ >
136
+ <div class="px-3 py-2 text-xs text-gray-400 dark:text-gray-500">
137
+ Selected (0)
138
+ </div>
139
+ <div id="workflow-filter-options" class="flex flex-col"></div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ <!-- Nodes filter dropdown -->
145
+ <div class="relative flex items-center gap-2">
146
+ <label
147
+ for="node-filter"
148
+ class="text-xs text-gray-400 dark:text-blue-500 rounded px-[6px] py-[9.25px] bg-gray-100 dark:bg-gray-800 font-semibold w-[74px] text-center"
149
+ >Nodes</label
150
+ >
151
+ <div class="relative">
152
+ <div
153
+ id="node-filter-trigger"
154
+ class="flex items-center justify-between border border-gray-300 dark:border-gray-600 rounded px-3 py-1.5 text-sm text-gray-800 dark:text-white w-56 cursor-pointer select-none"
155
+ >
156
+ <span id="node-filter-placeholder">All</span>
157
+ <svg
158
+ id="node-filter-chevron"
159
+ class="w-4 h-4 ml-2 transition-transform transform text-gray-500 dark:text-gray-300"
160
+ fill="none"
161
+ stroke="currentColor"
162
+ stroke-width="2"
163
+ viewBox="0 0 24 24"
164
+ >
165
+ <path
166
+ stroke-linecap="round"
167
+ stroke-linejoin="round"
168
+ d="M19 9l-7 7-7-7"
169
+ />
170
+ </svg>
171
+ </div>
172
+ <div
173
+ id="node-filter-dropdown"
174
+ class="absolute mt-1 z-50 bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded shadow-md w-full md:w-56 max-h-60 overflow-y-auto hidden"
175
+ >
176
+ <div class="px-3 py-2 text-xs text-gray-400 dark:text-gray-500">
177
+ Selected (0)
178
+ </div>
179
+ <div id="node-filter-options" class="flex flex-col"></div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </div>
184
+
185
+ <!-- Dark Mode Toggle -->
186
+ <button
187
+ onclick="toggleDarkMode()"
188
+ aria-label="Toggle Dark Mode"
189
+ class="flex items-center justify-center w-9 h-9 hover:bg-gray-100 dark:hover:bg-gray-800 transition hidden xl:block"
190
+ >
191
+ <span
192
+ id="darkModeIcon"
193
+ class="flex items-center justify-center h-9 text-gray-700 dark:text-gray-200"
194
+ ></span>
195
+ </button>
196
+ </div>
197
+ </nav>
198
+
199
+ <main class="pt-0 pb-6 px-6 py-6 space-y-6">
200
+ <script>
201
+ function toggleSection(id) {
202
+ const section = document.getElementById(id);
203
+ const isHidden = section.classList.toggle("hidden");
204
+
205
+ localStorage.setItem(`section:${id}`, isHidden);
206
+ }
207
+
208
+ function restoreToggles() {
209
+ document.querySelectorAll("[id$='-body']").forEach((section) => {
210
+ const saved = localStorage.getItem(`section:${section.id}`);
211
+ if (saved === "true") {
212
+ section.classList.add("hidden");
213
+ }
214
+ });
215
+ }
216
+
217
+ document.addEventListener("DOMContentLoaded", restoreToggles);
218
+
219
+ window.addEventListener("scroll", () => {
220
+ localStorage.setItem("scrollPosition", window.scrollY);
221
+ });
222
+
223
+ window.addEventListener("load", () => {
224
+ const savedScroll = localStorage.getItem("scrollPosition");
225
+ if (savedScroll !== null) {
226
+ setTimeout(() => {
227
+ requestAnimationFrame(() => {});
228
+ }, 200);
229
+ }
230
+ });
231
+ </script>
232
+
233
+ <!-- OVERVIEW -->
234
+ <section
235
+ class="bg-white dark:bg-gray-900 rounded-lg shadow-sm"
236
+ data-section-id="System Overview"
237
+ >
238
+ <div
239
+ class="flex items-center justify-between px-5 py-4 border-b border-gray-200 dark:border-gray-700"
240
+ >
241
+ <div>
242
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">
243
+ System Overview
244
+ </h2>
245
+ <p class="text-sm text-gray-500 dark:text-gray-400">
246
+ Aggregated metrics across all workflows and nodes in real time.
247
+ </p>
248
+ </div>
249
+
250
+ <button
251
+ onclick="toggleSection('overview-body', this)"
252
+ aria-label="Toggle Overview"
253
+ class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-300 transition"
254
+ >
255
+ <i data-feather="chevron-down" class="w-5 h-5"></i>
256
+ </button>
257
+ </div>
258
+
259
+ <div
260
+ id="overview-body"
261
+ class="p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
262
+ data-draggable-section="overview-body"
263
+ >
264
+ <!-- Total Requests -->
265
+ <div
266
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
267
+ data-id="overview_requests"
268
+ draggable="true"
269
+ >
270
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
271
+ Total Requests
272
+ </h3>
273
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
274
+ All workflow executions (last 1m).
275
+ </p>
276
+ <div class="flex-grow relative">
277
+ <canvas
278
+ id="overview_requests"
279
+ class="absolute inset-0 w-full h-full"
280
+ ></canvas>
281
+ </div>
282
+ </div>
283
+
284
+ <!-- Errors -->
285
+ <div
286
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
287
+ data-id="overview_errors"
288
+ draggable="true"
289
+ >
290
+ <h3 class="text-sm font-medium text-red-600 dark:text-red-400">
291
+ Error Count
292
+ </h3>
293
+ <p class="text-xs text-red-500 dark:text-red-300 mb-2">
294
+ Failures detected across workflows.
295
+ </p>
296
+ <div class="flex-grow relative">
297
+ <canvas
298
+ id="overview_errors"
299
+ class="absolute inset-0 w-full h-full"
300
+ ></canvas>
301
+ </div>
302
+ </div>
303
+
304
+ <!-- Risk -->
305
+ <div
306
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
307
+ data-id="overview_request_risk"
308
+ draggable="true"
309
+ >
310
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
311
+ Execution Error Risk (%)
312
+ </h3>
313
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
314
+ Failures as % of total requests.
315
+ </p>
316
+ <div class="flex-grow relative">
317
+ <canvas
318
+ id="overview_request_risk"
319
+ class="absolute inset-0 w-full h-full"
320
+ ></canvas>
321
+ </div>
322
+ </div>
323
+
324
+ <!-- Time -->
325
+ <div
326
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
327
+ data-id="overview_time"
328
+ draggable="true"
329
+ >
330
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
331
+ Avg. Execution Time (ms)
332
+ </h3>
333
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
334
+ Mean duration per workflow.
335
+ </p>
336
+ <div class="flex-grow relative">
337
+ <canvas
338
+ id="overview_time"
339
+ class="absolute inset-0 w-full h-full"
340
+ ></canvas>
341
+ </div>
342
+ </div>
343
+
344
+ <!-- CPU -->
345
+ <div
346
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
347
+ data-id="overview_cpu"
348
+ draggable="true"
349
+ >
350
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
351
+ CPU Usage
352
+ </h3>
353
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
354
+ Average CPU used (1m).
355
+ </p>
356
+ <div class="flex-grow relative">
357
+ <canvas
358
+ id="overview_cpu"
359
+ class="absolute inset-0 w-full h-full"
360
+ ></canvas>
361
+ </div>
362
+ </div>
363
+
364
+ <!-- CPU Risk -->
365
+ <div
366
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
367
+ data-id="overview_cpu_risk"
368
+ draggable="true"
369
+ >
370
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
371
+ CPU Risk (%)
372
+ </h3>
373
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
374
+ Used vs allocatable CPU cores.
375
+ </p>
376
+ <div class="flex-grow relative">
377
+ <canvas
378
+ id="overview_cpu_risk"
379
+ class="absolute inset-0 w-full h-full"
380
+ ></canvas>
381
+ </div>
382
+ </div>
383
+
384
+ <!-- Memory -->
385
+ <div
386
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
387
+ data-id="overview_memory"
388
+ draggable="true"
389
+ >
390
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
391
+ Memory Usage
392
+ </h3>
393
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
394
+ Avg memory used across workflows.
395
+ </p>
396
+ <div class="flex-grow relative">
397
+ <canvas
398
+ id="overview_memory"
399
+ class="absolute inset-0 w-full h-full"
400
+ ></canvas>
401
+ </div>
402
+ </div>
403
+
404
+ <!-- Memory Risk -->
405
+ <div
406
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
407
+ data-id="overview_memory_risk"
408
+ draggable="true"
409
+ >
410
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
411
+ Memory Risk (%)
412
+ </h3>
413
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
414
+ Used vs memory quota per node.
415
+ </p>
416
+ <div class="flex-grow relative">
417
+ <canvas
418
+ id="overview_memory_risk"
419
+ class="absolute inset-0 w-full h-full"
420
+ ></canvas>
421
+ </div>
422
+ </div>
423
+ </div>
424
+ </section>
425
+
426
+ <!-- WORKFLOWS -->
427
+ <section
428
+ class="bg-white dark:bg-gray-900 rounded-lg shadow-sm"
429
+ data-section-id="Workflow Metrics"
430
+ >
431
+ <div
432
+ class="flex items-center justify-between px-5 py-4 border-b border-gray-200 dark:border-gray-700"
433
+ >
434
+ <div>
435
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">
436
+ Workflow Metrics
437
+ </h2>
438
+ <p class="text-sm text-gray-500 dark:text-gray-400">
439
+ Insights per workflow — track usage, performance, and fault rates.
440
+ </p>
441
+ </div>
442
+ <button
443
+ onclick="toggleSection('workflows-body', this)"
444
+ aria-label="Toggle Workflows"
445
+ class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-300 transition"
446
+ >
447
+ <i data-feather="chevron-down" class="w-5 h-5"></i>
448
+ </button>
449
+ </div>
450
+
451
+ <div
452
+ id="workflows-body"
453
+ class="p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
454
+ data-draggable-section="workflows-body"
455
+ >
456
+ <!-- Total Requests -->
457
+ <div
458
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
459
+ data-id="workflow_requests"
460
+ draggable="true"
461
+ >
462
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
463
+ Requests per Workflow
464
+ </h3>
465
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
466
+ Number of executions per workflow.
467
+ </p>
468
+ <div class="flex-grow relative">
469
+ <canvas
470
+ id="workflow_requests"
471
+ class="absolute inset-0 w-full h-full"
472
+ ></canvas>
473
+ </div>
474
+ </div>
475
+
476
+ <!-- Errors -->
477
+ <div
478
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
479
+ data-id="workflow_errors"
480
+ draggable="true"
481
+ >
482
+ <h3 class="text-sm font-medium text-red-600 dark:text-red-400">
483
+ Errors per Workflow
484
+ </h3>
485
+ <p class="text-xs text-red-500 dark:text-red-300 mb-2">
486
+ Failures by workflow.
487
+ </p>
488
+ <div class="flex-grow relative">
489
+ <canvas
490
+ id="workflow_errors"
491
+ class="absolute inset-0 w-full h-full"
492
+ ></canvas>
493
+ </div>
494
+ </div>
495
+
496
+ <!-- Error Risk -->
497
+ <div
498
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
499
+ data-id="workflow_request_risk"
500
+ draggable="true"
501
+ >
502
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
503
+ Execution Error Risk (%)
504
+ </h3>
505
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
506
+ Failures as % of total attempts per workflow.
507
+ </p>
508
+ <div class="flex-grow relative">
509
+ <canvas
510
+ id="workflow_request_risk"
511
+ class="absolute inset-0 w-full h-full"
512
+ ></canvas>
513
+ </div>
514
+ </div>
515
+
516
+ <!-- Execution Time -->
517
+ <div
518
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
519
+ data-id="workflow_time"
520
+ draggable="true"
521
+ >
522
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
523
+ Execution Time (s)
524
+ </h3>
525
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
526
+ Avg. duration per workflow.
527
+ </p>
528
+ <div class="flex-grow relative">
529
+ <canvas
530
+ id="workflow_time"
531
+ class="absolute inset-0 w-full h-full"
532
+ ></canvas>
533
+ </div>
534
+ </div>
535
+
536
+ <!-- CPU Usage -->
537
+ <div
538
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
539
+ data-id="workflow_cpu"
540
+ draggable="true"
541
+ >
542
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
543
+ CPU Usage (s)
544
+ </h3>
545
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
546
+ Avg. CPU consumption by workflow.
547
+ </p>
548
+ <div class="flex-grow relative">
549
+ <canvas
550
+ id="workflow_cpu"
551
+ class="absolute inset-0 w-full h-full"
552
+ ></canvas>
553
+ </div>
554
+ </div>
555
+
556
+ <!-- CPU Risk -->
557
+ <div
558
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
559
+ data-id="workflow_cpu_risk"
560
+ draggable="true"
561
+ >
562
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
563
+ CPU Risk (%)
564
+ </h3>
565
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
566
+ % of allocated CPU cores used.
567
+ </p>
568
+ <div class="flex-grow relative">
569
+ <canvas
570
+ id="workflow_cpu_risk"
571
+ class="absolute inset-0 w-full h-full"
572
+ ></canvas>
573
+ </div>
574
+ </div>
575
+
576
+ <!-- Memory Usage -->
577
+ <div
578
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
579
+ data-id="workflow_memory"
580
+ draggable="true"
581
+ >
582
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
583
+ Memory Usage (MB)
584
+ </h3>
585
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
586
+ Avg. memory per workflow.
587
+ </p>
588
+ <div class="flex-grow relative">
589
+ <canvas
590
+ id="workflow_memory"
591
+ class="absolute inset-0 w-full h-full"
592
+ ></canvas>
593
+ </div>
594
+ </div>
595
+
596
+ <!-- Memory Risk -->
597
+ <div
598
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
599
+ data-id="workflow_memory_risk"
600
+ draggable="true"
601
+ >
602
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
603
+ Memory Risk (%)
604
+ </h3>
605
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
606
+ % of memory quota utilized.
607
+ </p>
608
+ <div class="flex-grow relative">
609
+ <canvas
610
+ id="workflow_memory_risk"
611
+ class="absolute inset-0 w-full h-full"
612
+ ></canvas>
613
+ </div>
614
+ </div>
615
+ </div>
616
+ </section>
617
+
618
+ <!-- NODES -->
619
+ <section
620
+ class="bg-white dark:bg-gray-900 rounded-lg shadow-sm"
621
+ data-section-id="Node Metrics"
622
+ >
623
+ <div
624
+ class="flex items-center justify-between px-5 py-4 border-b border-gray-200 dark:border-gray-700"
625
+ >
626
+ <div>
627
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">
628
+ Node Metrics
629
+ </h2>
630
+ <p class="text-sm text-gray-500 dark:text-gray-400">
631
+ Monitor blok performance and health by individual node.
632
+ </p>
633
+ </div>
634
+ <button
635
+ onclick="toggleSection('nodes-body', this)"
636
+ aria-label="Toggle Nodes"
637
+ class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-300 transition"
638
+ >
639
+ <i data-feather="chevron-down" class="w-5 h-5"></i>
640
+ </button>
641
+ </div>
642
+
643
+ <div
644
+ id="nodes-body"
645
+ class="p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
646
+ data-draggable-section="nodes-body"
647
+ >
648
+ <!-- Requests -->
649
+ <div
650
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
651
+ data-id="node_requests"
652
+ draggable="true"
653
+ >
654
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
655
+ Requests per Node
656
+ </h3>
657
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
658
+ How often each node executes.
659
+ </p>
660
+ <div class="flex-grow relative">
661
+ <canvas
662
+ id="node_requests"
663
+ class="absolute inset-0 w-full h-full"
664
+ ></canvas>
665
+ </div>
666
+ </div>
667
+
668
+ <!-- Errors -->
669
+ <div
670
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
671
+ data-id="node_errors"
672
+ draggable="true"
673
+ >
674
+ <h3 class="text-sm font-medium text-red-600 dark:text-red-400">
675
+ Errors per Node
676
+ </h3>
677
+ <p class="text-xs text-red-500 dark:text-red-300 mb-2">
678
+ Failures encountered per node.
679
+ </p>
680
+ <div class="flex-grow relative">
681
+ <canvas
682
+ id="node_errors"
683
+ class="absolute inset-0 w-full h-full"
684
+ ></canvas>
685
+ </div>
686
+ </div>
687
+
688
+ <!-- Execution Risk -->
689
+ <div
690
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
691
+ data-id="node_request_risk"
692
+ draggable="true"
693
+ >
694
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
695
+ Execution Error Risk (%)
696
+ </h3>
697
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
698
+ Failed executions as percentage of total.
699
+ </p>
700
+ <div class="flex-grow relative">
701
+ <canvas
702
+ id="node_request_risk"
703
+ class="absolute inset-0 w-full h-full"
704
+ ></canvas>
705
+ </div>
706
+ </div>
707
+
708
+ <!-- Execution Time -->
709
+ <div
710
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
711
+ data-id="node_time"
712
+ draggable="true"
713
+ >
714
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
715
+ Execution Time (s)
716
+ </h3>
717
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
718
+ Average duration of node execution.
719
+ </p>
720
+ <div class="flex-grow relative">
721
+ <canvas
722
+ id="node_time"
723
+ class="absolute inset-0 w-full h-full"
724
+ ></canvas>
725
+ </div>
726
+ </div>
727
+
728
+ <!-- CPU Usage -->
729
+ <div
730
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
731
+ data-id="node_cpu"
732
+ draggable="true"
733
+ >
734
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
735
+ CPU Usage (s)
736
+ </h3>
737
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
738
+ Avg. CPU load per node.
739
+ </p>
740
+ <div class="flex-grow relative">
741
+ <canvas
742
+ id="node_cpu"
743
+ class="absolute inset-0 w-full h-full"
744
+ ></canvas>
745
+ </div>
746
+ </div>
747
+
748
+ <!-- CPU Risk -->
749
+ <div
750
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
751
+ data-id="node_cpu_risk"
752
+ draggable="true"
753
+ >
754
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
755
+ CPU Risk (%)
756
+ </h3>
757
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
758
+ CPU usage as % of allocated cores.
759
+ </p>
760
+ <div class="flex-grow relative">
761
+ <canvas
762
+ id="node_cpu_risk"
763
+ class="absolute inset-0 w-full h-full"
764
+ ></canvas>
765
+ </div>
766
+ </div>
767
+
768
+ <!-- Memory Usage -->
769
+ <div
770
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
771
+ data-id="node_memory"
772
+ draggable="true"
773
+ >
774
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
775
+ Memory Usage (MB)
776
+ </h3>
777
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
778
+ Avg. RAM usage per node.
779
+ </p>
780
+ <div class="flex-grow relative">
781
+ <canvas
782
+ id="node_memory"
783
+ class="absolute inset-0 w-full h-full"
784
+ ></canvas>
785
+ </div>
786
+ </div>
787
+
788
+ <!-- Memory Risk -->
789
+ <div
790
+ class="bg-white dark:bg-gray-800 p-4 rounded shadow flex flex-col h-[300px]"
791
+ data-id="node_memory_risk"
792
+ draggable="true"
793
+ >
794
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
795
+ Memory Risk (%)
796
+ </h3>
797
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
798
+ Memory used as % of available memory per node.
799
+ </p>
800
+ <div class="flex-grow relative">
801
+ <canvas
802
+ id="node_memory_risk"
803
+ class="absolute inset-0 w-full h-full"
804
+ ></canvas>
805
+ </div>
806
+ </div>
807
+ </div>
808
+ </section>
809
+
810
+ <section
811
+ class="bg-white dark:bg-gray-900 rounded-lg shadow-sm mt-6 overflow-x-auto"
812
+ data-section-id="Workflow Table"
813
+ >
814
+ <div class="px-5 py-4 border-b border-gray-200 dark:border-gray-700">
815
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">
816
+ Workflow Metrics
817
+ </h2>
818
+ <p class="text-sm text-gray-600 dark:text-gray-400">
819
+ Real-time snapshot of key performance indicators across all
820
+ workflows.
821
+ </p>
822
+ </div>
823
+
824
+ <table
825
+ id="workflow-metrics-table"
826
+ class="min-w-full text-sm text-left divide-y divide-gray-200 dark:divide-gray-800"
827
+ >
828
+ <thead
829
+ class="bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-xs uppercase tracking-wider"
830
+ >
831
+ <tr>
832
+ <th scope="col" class="px-4 py-3 font-medium">Workflow</th>
833
+ <th scope="col" class="px-4 py-3 font-medium text-right">
834
+ Requests
835
+ </th>
836
+ <th scope="col" class="px-4 py-3 font-medium text-right">
837
+ Errors
838
+ </th>
839
+ <th scope="col" class="px-4 py-3 font-medium text-right">
840
+ Exec Time (ms)
841
+ </th>
842
+ <th scope="col" class="px-4 py-3 font-medium text-right">
843
+ CPU (%)
844
+ </th>
845
+ <th scope="col" class="px-4 py-3 font-medium text-right">
846
+ Memory (MB)
847
+ </th>
848
+ </tr>
849
+ </thead>
850
+ <tbody
851
+ id="workflow-metrics-body"
852
+ class="divide-y divide-gray-100 dark:divide-gray-700 bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-100"
853
+ >
854
+ <!-- JavaScript dynamically fills this -->
855
+ </tbody>
856
+ </table>
857
+ </section>
858
+
859
+ <section
860
+ class="bg-white dark:bg-gray-900 rounded-lg shadow-sm mt-6 overflow-x-auto"
861
+ data-section-id="Node Table"
862
+ >
863
+ <div class="px-5 py-4 border-b border-gray-200 dark:border-gray-700">
864
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">
865
+ Nodes Metrics
866
+ </h2>
867
+ <p class="text-sm text-gray-600 dark:text-gray-400">
868
+ Real-time snapshot of key performance indicators across all nodes.
869
+ </p>
870
+ </div>
871
+
872
+ <table
873
+ id="node-metrics-table"
874
+ class="min-w-full text-sm text-left divide-y divide-gray-200 dark:divide-gray-800"
875
+ >
876
+ <thead
877
+ class="bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-xs uppercase tracking-wider"
878
+ >
879
+ <tr>
880
+ <th scope="col" class="px-4 py-3 font-medium">Workflow</th>
881
+ <th scope="col" class="px-4 py-3 font-medium text-right">
882
+ Requests
883
+ </th>
884
+ <th scope="col" class="px-4 py-3 font-medium text-right">
885
+ Errors
886
+ </th>
887
+ <th scope="col" class="px-4 py-3 font-medium text-right">
888
+ Exec Time (ms)
889
+ </th>
890
+ <th scope="col" class="px-4 py-3 font-medium text-right">
891
+ CPU (%)
892
+ </th>
893
+ <th scope="col" class="px-4 py-3 font-medium text-right">
894
+ Memory (MB)
895
+ </th>
896
+ </tr>
897
+ </thead>
898
+ <tbody
899
+ id="nodes-metrics-body"
900
+ class="divide-y divide-gray-100 dark:divide-gray-700 bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-100"
901
+ >
902
+ <!-- JavaScript dynamically fills this -->
903
+ </tbody>
904
+ </table>
905
+ </section>
906
+
907
+ <!-- System Logs -->
908
+ <section
909
+ class="bg-white dark:bg-gray-900 rounded shadow"
910
+ data-section-id="System Logs"
911
+ >
912
+ <div
913
+ class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700"
914
+ >
915
+ <div class="flex items-center gap-2">
916
+ <!-- Info icon -->
917
+ <svg
918
+ class="w-5 h-5 text-blue-500 dark:text-blue-400"
919
+ fill="currentColor"
920
+ viewBox="0 0 20 20"
921
+ aria-hidden="true"
922
+ >
923
+ <path
924
+ d="M10 2a8 8 0 100 16 8 8 0 000-16zM9 7h2v6H9V7zm0 7h2v2H9v-2z"
925
+ />
926
+ </svg>
927
+ <div>
928
+ <h2 class="text-base font-semibold text-gray-900 dark:text-white">
929
+ System Logs
930
+ </h2>
931
+ <p class="text-sm text-gray-600 dark:text-gray-300">
932
+ Most recent 50 log entries from Loki (info level).
933
+ </p>
934
+ </div>
935
+ </div>
936
+ <button
937
+ onclick="toggleSection('loki-body')"
938
+ aria-label="Toggle Overview"
939
+ class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-300 transition"
940
+ >
941
+ <i data-feather="chevron-down" class="w-5 h-5"></i>
942
+ </button>
943
+ </div>
944
+ <div
945
+ id="loki-body"
946
+ class="p-4 max-h-[400px] overflow-y-auto bg-black text-gray-300 text-xs font-mono rounded"
947
+ >
948
+ <ul id="lokiLogs" class="space-y-1"></ul>
949
+ </div>
950
+ </section>
951
+
952
+ <!-- Error Logs -->
953
+ <section
954
+ class="bg-white dark:bg-gray-900 rounded shadow mt-6"
955
+ data-section-id="Error Logs"
956
+ >
957
+ <div
958
+ class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700"
959
+ >
960
+ <div class="flex items-center gap-2">
961
+ <!-- Error icon -->
962
+ <svg
963
+ class="w-5 h-5 text-red-500 dark:text-red-400"
964
+ fill="currentColor"
965
+ viewBox="0 0 20 20"
966
+ aria-hidden="true"
967
+ >
968
+ <path
969
+ fill-rule="evenodd"
970
+ d="M10 2a8 8 0 100 16 8 8 0 000-16zM9 7h2v4H9V7zm0 5h2v2H9v-2z"
971
+ clip-rule="evenodd"
972
+ />
973
+ </svg>
974
+ <div>
975
+ <h2
976
+ class="text-base font-semibold text-red-600 dark:text-red-400"
977
+ >
978
+ Error Logs
979
+ </h2>
980
+ <p class="text-sm text-gray-600 dark:text-gray-300">
981
+ Last 50 error-level entries logged to Loki.
982
+ </p>
983
+ </div>
984
+ </div>
985
+ <button
986
+ onclick="toggleSection('loki-error-body')"
987
+ aria-label="Toggle Overview"
988
+ class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-300 transition"
989
+ >
990
+ <i data-feather="chevron-down" class="w-5 h-5"></i>
991
+ </button>
992
+ </div>
993
+ <div
994
+ id="loki-error-body"
995
+ class="p-4 max-h-[400px] overflow-y-auto bg-black text-red-300 text-xs font-mono rounded"
996
+ >
997
+ <ul id="lokiErrorLogs" class="space-y-1"></ul>
998
+ </div>
999
+ </section>
1000
+ </main>
1001
+
1002
+ <script>
1003
+ const prometheusBase = "http://localhost:4040";
1004
+ const prometheusBaseToken = undefined;
1005
+
1006
+ const queries = {
1007
+ overview: {
1008
+ requests: "sum(increase(workflow_total[1m]))",
1009
+ time: `sum(avg_over_time(workflow_time[1m]))`,
1010
+ errors: "sum(increase(workflow_errors_total[1m]))",
1011
+ cpu: "sum(avg_over_time(workflow_cpu[1m]))",
1012
+ memory: "sum(avg_over_time(workflow_memory[1m]))",
1013
+ risk_cpu:
1014
+ "sum(avg_over_time(workflow_cpu[1m])) / sum(avg_over_time(workflow_cpu_total[1m])) * 100",
1015
+ risk_memory:
1016
+ "sum(avg_over_time(workflow_memory[1m])) / sum(avg_over_time(workflow_memory_total[1m])) * 100",
1017
+ request_risk:
1018
+ "sum(increase(workflow_errors_total[1m])) / (sum(increase(workflow_total[1m])) + sum(increase(workflow_errors_total[1m]))) * 100",
1019
+ },
1020
+ workflows: {
1021
+ requests:
1022
+ "(sum(increase(workflow_total[1m])) by (workflow_path)) > 0",
1023
+ time: "sum(avg_over_time(workflow_time[1m])) by (workflow_path)",
1024
+ errors:
1025
+ "(sum(increase(workflow_errors_total[1m])) by (workflow_path)) > 0",
1026
+ cpu: "sum(avg_over_time(workflow_cpu[1m])) by (workflow_path)",
1027
+ memory: "sum(avg_over_time(workflow_memory[1m])) by (workflow_path)",
1028
+ risk_cpu:
1029
+ "sum by (workflow_path)(avg_over_time(workflow_cpu[1m])) / sum by (workflow_path)(avg_over_time(workflow_cpu_total[1m])) * 100",
1030
+ risk_memory:
1031
+ "sum by (workflow_path)(avg_over_time(workflow_memory[1m])) / sum by (workflow_path)(avg_over_time(workflow_memory_total[1m])) * 100",
1032
+ request_risk:
1033
+ "(sum(increase(workflow_errors_total[1m])) by (workflow_path) / (sum(increase(workflow_total[1m])) by (workflow_path) + sum(increase(workflow_errors_total[1m])) by (workflow_path)) * 100) > 0",
1034
+ },
1035
+ nodes: {
1036
+ requests: "(sum(increase(node_total[1m])) by (node_name)) > 0",
1037
+ time: "sum(increase(node_time[1m])) by (node_name)",
1038
+ errors: "(sum(increase(node_errors_total[1m])) by (node_name)) > 0",
1039
+ cpu: "avg_over_time(node_cpu[1m])",
1040
+ memory: "sum(increase(node_memory[1m])) by (node_name)",
1041
+ risk_cpu:
1042
+ "sum by (node_name, workflow_path)(avg_over_time(node_cpu[1m])) / sum by (node_name, workflow_path)(avg_over_time(node_cpu_total[1m])) * 100",
1043
+ risk_memory:
1044
+ "sum by (node_name, workflow_path)(avg_over_time(node_memory[1m])) / sum by (node_name, workflow_path)(avg_over_time(node_memory_total[1m])) * 100",
1045
+ request_risk:
1046
+ "(sum(increase(node_errors_total[1m])) by (node_name) / (sum(increase(node_total[1m])) by (node_name) + sum(increase(node_errors_total[1m])) by (node_name)) * 100) > 0",
1047
+ },
1048
+ };
1049
+
1050
+ const charts = {};
1051
+
1052
+ function createChart(ctx, label, useThreshold = false) {
1053
+ let opts1 = {
1054
+ type: "line",
1055
+ data: { datasets: [] },
1056
+ options: {
1057
+ animation: false,
1058
+ elements: {
1059
+ point: {
1060
+ radius: 0,
1061
+ hoverRadius: 6,
1062
+ hitRadius: 6,
1063
+ },
1064
+ },
1065
+ interaction: {
1066
+ mode: "nearest",
1067
+ intersect: false,
1068
+ },
1069
+ responsive: true,
1070
+ maintainAspectRatio: false,
1071
+ scales: {
1072
+ x: {
1073
+ type: "time",
1074
+ time: { unit: "minute" },
1075
+ title: { display: true, text: "Time" },
1076
+ },
1077
+ y: { beginAtZero: true },
1078
+ },
1079
+ plugins: {
1080
+ legend: {
1081
+ display: true,
1082
+ },
1083
+ tooltip: {
1084
+ enabled: true,
1085
+ },
1086
+ },
1087
+ },
1088
+ };
1089
+
1090
+ let opts2 = {
1091
+ type: "line",
1092
+ data: { datasets: [] },
1093
+ options: {
1094
+ animation: false,
1095
+ responsive: true,
1096
+ maintainAspectRatio: false,
1097
+ scales: {
1098
+ x: {
1099
+ type: "time",
1100
+ time: { unit: "minute" },
1101
+ title: { display: true, text: "Time" },
1102
+ },
1103
+ y: {
1104
+ beginAtZero: true,
1105
+ max: 100,
1106
+ title: { display: true, text: "Risk (%)" },
1107
+ },
1108
+ },
1109
+ plugins: {
1110
+ legend: { display: true },
1111
+ tooltip: { enabled: true },
1112
+ },
1113
+ },
1114
+ plugins: useThreshold ? [thresholdBackgroundPlugin] : [],
1115
+ };
1116
+
1117
+ let opts3 = {
1118
+ type: "line",
1119
+ data: { datasets: [] },
1120
+ options: {
1121
+ animation: false,
1122
+ responsive: true,
1123
+ maintainAspectRatio: false,
1124
+ scales: {
1125
+ x: {
1126
+ type: "time",
1127
+ time: { unit: "minute" },
1128
+ title: { display: true, text: "Time" },
1129
+ },
1130
+ y: {
1131
+ beginAtZero: true,
1132
+ max: 100,
1133
+ title: { display: true, text: "Risk (%)" },
1134
+ },
1135
+ },
1136
+ plugins: {
1137
+ legend: { display: true },
1138
+ tooltip: { enabled: true },
1139
+ },
1140
+ },
1141
+ plugins: useThreshold ? [thresholdErrorsBackgroundPlugin] : [],
1142
+ };
1143
+
1144
+ if (
1145
+ label.includes("overview_request_risk") ||
1146
+ label.includes("workflow_request_risk") ||
1147
+ label.includes("node_request_risk")
1148
+ ) {
1149
+ opts2 = opts3;
1150
+ }
1151
+
1152
+ return new Chart(ctx, useThreshold ? opts2 : opts1);
1153
+ }
1154
+
1155
+ function parseSeries(result, groupLabel, color) {
1156
+ const palette = [
1157
+ "#3b82f6",
1158
+ "#10b981",
1159
+ "#f59e0b",
1160
+ "#8b5cf6",
1161
+ "#ec4899",
1162
+ "#ef4444",
1163
+ ];
1164
+ let i = 0;
1165
+
1166
+ return result.map((serie) => {
1167
+ const label = serie.metric[groupLabel] || "unknown";
1168
+ const datasetColor = color || palette[i++ % palette.length];
1169
+ return {
1170
+ label,
1171
+ data: serie.values.map(([ts, val]) => ({
1172
+ x: new Date(ts * 1000),
1173
+ y: parseFloat(val),
1174
+ })),
1175
+ borderColor: datasetColor,
1176
+ backgroundColor: datasetColor + "33",
1177
+ fill: true,
1178
+ };
1179
+ });
1180
+ }
1181
+
1182
+ async function fetchMetric(query) {
1183
+ const end = Math.floor(Date.now() / 1000);
1184
+ const start = end - 600;
1185
+ const params = new URLSearchParams({ query, start, end, step: "60" });
1186
+ const res = prometheusBaseToken
1187
+ ? await fetch(`${prometheusBase}/api/metrics?${params}`, {
1188
+ headers: {
1189
+ Authorization: `Bearer ${prometheusBaseToken}`,
1190
+ Accept: "application/json",
1191
+ "Accept-Encoding": "identity",
1192
+ },
1193
+ })
1194
+ : await fetch(`${prometheusBase}/api/metrics?${params}`);
1195
+ const json = await res.json();
1196
+ return json.data?.result || [];
1197
+ }
1198
+
1199
+ function buildMatcher(label, valuesSet) {
1200
+ if (!valuesSet || !(valuesSet instanceof Set)) return ""; // ⚠️ Previene el error
1201
+
1202
+ const values = Array.from(valuesSet);
1203
+ if (values.includes("__all__")) return "";
1204
+ if (values.length === 0) return `${label}=~"^$"`;
1205
+
1206
+ const regex = values
1207
+ .map((v) =>
1208
+ v.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&").replace(/\\/g, "\\\\")
1209
+ )
1210
+ .join("|");
1211
+
1212
+ return `${label}=~"${regex}"`;
1213
+ }
1214
+
1215
+ async function updateChart(
1216
+ id,
1217
+ query,
1218
+ isGrouped = false,
1219
+ groupLabel = "",
1220
+ forceColor = null
1221
+ ) {
1222
+ const canvas = document.getElementById(id);
1223
+ if (!canvas) return;
1224
+ const ctx = canvas.getContext("2d");
1225
+
1226
+ if (!charts[id]) {
1227
+ let thresholdPluginActive = id.includes("risk");
1228
+ charts[id] = createChart(ctx, id, thresholdPluginActive);
1229
+ }
1230
+
1231
+ let finalQuery = query;
1232
+
1233
+ if (id.startsWith("workflow_")) {
1234
+ const matcher = buildMatcher(
1235
+ "workflow_path",
1236
+ window.selectedFilters?.workflow
1237
+ );
1238
+
1239
+ if (matcher) {
1240
+ finalQuery = query.replace(
1241
+ /workflow_(total|errors_total|time|cpu_total|cpu|memory_total|memory)\b/g,
1242
+ (metric) => `${metric}{${matcher}}`
1243
+ );
1244
+ }
1245
+ }
1246
+
1247
+ if (id.startsWith("overview_")) {
1248
+ const matcher = buildMatcher(
1249
+ "workflow_path",
1250
+ window.selectedFilters?.workflow
1251
+ );
1252
+
1253
+ if (matcher) {
1254
+ finalQuery = query.replace(
1255
+ /workflow_(total|errors_total|time|cpu_total|cpu|memory_total|memory)\b/g,
1256
+ (metric) => `${metric}{${matcher}}`
1257
+ );
1258
+ }
1259
+ }
1260
+
1261
+ if (id.startsWith("node_")) {
1262
+ const matcher = buildMatcher(
1263
+ "node_name",
1264
+ window.selectedFilters?.node
1265
+ );
1266
+
1267
+ if (matcher) {
1268
+ finalQuery = query.replace(
1269
+ /node_(total|errors_total|time|cpu_total|cpu|memory_total|memory)\b/g,
1270
+ (metric) => `${metric}{${matcher}}`
1271
+ );
1272
+ }
1273
+ }
1274
+
1275
+ const result = await fetchMetric(finalQuery);
1276
+ const datasets = isGrouped
1277
+ ? parseSeries(result, groupLabel, forceColor)
1278
+ : [
1279
+ {
1280
+ label: id,
1281
+ data:
1282
+ result[0]?.values.map(([ts, val]) => ({
1283
+ x: new Date(ts * 1000),
1284
+ y: parseFloat(val),
1285
+ })) || [],
1286
+ borderColor: forceColor || "#2563eb",
1287
+ backgroundColor: (forceColor || "#2563eb") + "33",
1288
+ fill: true,
1289
+ },
1290
+ ];
1291
+
1292
+ charts[id].data.datasets = datasets;
1293
+ charts[id].update();
1294
+ }
1295
+
1296
+ async function updateAllCharts() {
1297
+ // OVERVIEW
1298
+ await updateChart("overview_requests", queries.overview.requests);
1299
+ await updateChart("overview_time", queries.overview.time);
1300
+ await updateChart(
1301
+ "overview_errors",
1302
+ queries.overview.errors,
1303
+ false,
1304
+ "",
1305
+ "#dc2626"
1306
+ );
1307
+ await updateChart("overview_cpu", queries.overview.cpu);
1308
+ await updateChart("overview_memory", queries.overview.memory);
1309
+
1310
+ // WORKFLOWS
1311
+ await updateChart(
1312
+ "workflow_requests",
1313
+ queries.workflows.requests,
1314
+ true,
1315
+ "workflow_path"
1316
+ );
1317
+ await updateChart(
1318
+ "workflow_time",
1319
+ queries.workflows.time,
1320
+ true,
1321
+ "workflow_path"
1322
+ );
1323
+ await updateChart(
1324
+ "workflow_errors",
1325
+ queries.workflows.errors,
1326
+ true,
1327
+ "workflow_path",
1328
+ "#dc2626"
1329
+ );
1330
+ await updateChart(
1331
+ "workflow_cpu",
1332
+ queries.workflows.cpu,
1333
+ true,
1334
+ "workflow_path"
1335
+ );
1336
+ await updateChart(
1337
+ "workflow_memory",
1338
+ queries.workflows.memory,
1339
+ true,
1340
+ "workflow_path"
1341
+ );
1342
+
1343
+ // NODES
1344
+ await updateChart(
1345
+ "node_requests",
1346
+ queries.nodes.requests,
1347
+ true,
1348
+ "node_name"
1349
+ );
1350
+ await updateChart("node_time", queries.nodes.time, true, "node_name");
1351
+ await updateChart(
1352
+ "node_memory",
1353
+ queries.nodes.memory,
1354
+ true,
1355
+ "node_name"
1356
+ );
1357
+ await updateChart("node_cpu", queries.nodes.cpu, true, "node_name");
1358
+ await updateChart(
1359
+ "node_errors",
1360
+ queries.nodes.errors,
1361
+ true,
1362
+ "node_name"
1363
+ );
1364
+
1365
+ // RISK
1366
+ await updateChart(
1367
+ "node_cpu_risk",
1368
+ queries.nodes.risk_cpu,
1369
+ true,
1370
+ "node_name"
1371
+ );
1372
+ await updateChart(
1373
+ "node_memory_risk",
1374
+ queries.nodes.risk_memory,
1375
+ true,
1376
+ "node_name"
1377
+ );
1378
+ await updateChart(
1379
+ "node_request_risk",
1380
+ queries.nodes.request_risk,
1381
+ true,
1382
+ "node_name"
1383
+ );
1384
+
1385
+ await updateChart(
1386
+ "workflow_cpu_risk",
1387
+ queries.workflows.risk_cpu,
1388
+ true,
1389
+ "workflow_path"
1390
+ );
1391
+ await updateChart(
1392
+ "workflow_memory_risk",
1393
+ queries.workflows.risk_memory,
1394
+ true,
1395
+ "workflow_path"
1396
+ );
1397
+ await updateChart(
1398
+ "workflow_request_risk",
1399
+ queries.workflows.request_risk,
1400
+ true,
1401
+ "workflow_path"
1402
+ );
1403
+
1404
+ await updateChart("overview_cpu_risk", queries.overview.risk_cpu);
1405
+ await updateChart("overview_memory_risk", queries.overview.risk_memory);
1406
+ await updateChart(
1407
+ "overview_request_risk",
1408
+ queries.overview.request_risk
1409
+ );
1410
+ }
1411
+
1412
+ async function updateLokiLogs() {
1413
+ try {
1414
+ const end = BigInt(Date.now()) * 1000000n;
1415
+ const start = end - 3600n * 1000000000n;
1416
+
1417
+ const params = new URLSearchParams({
1418
+ query: '{service_name="blok-http"} | json | level="info"',
1419
+ limit: "50",
1420
+ start: start.toString(),
1421
+ end: end.toString(),
1422
+ direction: "backward",
1423
+ });
1424
+
1425
+ const res = await fetch(
1426
+ `http://localhost:3200/loki/api/v1/query_range?${params}`
1427
+ );
1428
+ const json = await res.json();
1429
+
1430
+ const container = document.getElementById("lokiLogs");
1431
+ container.innerHTML = "";
1432
+
1433
+ const streams = json.data?.result || [];
1434
+
1435
+ for (let i = 0; i < streams.length; i++) {
1436
+ const stream = streams[i];
1437
+ for (let j = 0; j < stream.values.length; j++) {
1438
+ const [ts, line] = stream.values[j];
1439
+ const date = new Date(Number(ts) / 1000000);
1440
+ const item = document.createElement("li");
1441
+ const log_model = JSON.parse(line);
1442
+
1443
+ item.textContent = `[${date.toLocaleTimeString()}] ${
1444
+ log_model.app
1445
+ }:${log_model.env}:${log_model.workflow_path} ${
1446
+ log_model.message
1447
+ }`;
1448
+ container.appendChild(item);
1449
+ }
1450
+ }
1451
+
1452
+ document.getElementById("loki-body").scrollTop =
1453
+ document.getElementById("loki-body").scrollHeight;
1454
+ } catch (err) {
1455
+ console.error("Failed to load Loki logs:", err);
1456
+ }
1457
+ }
1458
+
1459
+ async function updateLokiErrors() {
1460
+ try {
1461
+ const end = BigInt(Date.now()) * 1000000n;
1462
+ const start = end - 3600n * 1000000000n;
1463
+
1464
+ const params = new URLSearchParams({
1465
+ query: '{service_name="blok-http"} | json | level="error"',
1466
+ limit: "50",
1467
+ start: start.toString(),
1468
+ end: end.toString(),
1469
+ direction: "backward",
1470
+ });
1471
+
1472
+ const res = await fetch(
1473
+ `http://localhost:3200/loki/api/v1/query_range?${params}`
1474
+ );
1475
+ const json = await res.json();
1476
+
1477
+ const container = document.getElementById("lokiErrorLogs");
1478
+ container.innerHTML = "";
1479
+
1480
+ const streams = json.data?.result || [];
1481
+
1482
+ for (let i = 0; i < streams.length; i++) {
1483
+ const stream = streams[i];
1484
+ for (let j = 0; j < stream.values.length; j++) {
1485
+ const [ts, line] = stream.values[j];
1486
+ const date = new Date(Number(ts) / 1000000);
1487
+ const item = document.createElement("li");
1488
+ const log_model = JSON.parse(line);
1489
+
1490
+ item.textContent = `[${date.toLocaleTimeString()}] ${
1491
+ log_model.app
1492
+ }:${log_model.env}:${
1493
+ log_model.workflow_path || log_model.workflow_name
1494
+ } ${log_model.message}: ${log_model.stack}`;
1495
+ container.appendChild(item);
1496
+ }
1497
+ }
1498
+
1499
+ document.getElementById("loki-error-body").scrollTop =
1500
+ document.getElementById("loki-error-body").scrollHeight;
1501
+ } catch (err) {
1502
+ console.error("Failed to load Loki error logs:", err);
1503
+ }
1504
+ }
1505
+
1506
+ async function updateWorkflowTable() {
1507
+ const queries = {
1508
+ requests: "sum(increase(workflow_total[1m])) by (workflow_path)",
1509
+ errors: "sum(increase(workflow_errors_total[1m])) by (workflow_path)",
1510
+ time: "sum(increase(workflow_time[1m])) by (workflow_path)",
1511
+ cpu: "(sum(increase(workflow_cpu[1m])) by (workflow_path) / sum(increase(workflow_total[1m])) by (workflow_path)) * 100",
1512
+ memory: "sum(increase(workflow_memory[1m])) by (workflow_path)",
1513
+ };
1514
+
1515
+ const endpoint = `${prometheusBase}/api/metrics`;
1516
+ const selectedWorkflows =
1517
+ window.selectedFilters?.workflow || new Set(["__all__"]);
1518
+ const filterMatcher = buildMatcher("workflow_path", selectedWorkflows);
1519
+
1520
+ const results = {};
1521
+
1522
+ await Promise.all(
1523
+ Object.entries(queries).map(async ([key, query]) => {
1524
+ const effectiveQuery = filterMatcher
1525
+ ? query.replace(
1526
+ /workflow_(total|errors_total|time|cpu|memory)/g,
1527
+ (match) => `${match}{${filterMatcher}}`
1528
+ )
1529
+ : query;
1530
+
1531
+ const res = await fetch(
1532
+ `${endpoint}?query=${encodeURIComponent(effectiveQuery)}`,
1533
+ {
1534
+ headers: {
1535
+ "x-table": "true",
1536
+ },
1537
+ }
1538
+ );
1539
+ const json = await res.json();
1540
+ if (json.status === "success") {
1541
+ for (const entry of json.data.result) {
1542
+ const workflow =
1543
+ key === "requests"
1544
+ ? entry.metric.workflow_path || "unknown"
1545
+ : entry.metric.workflow_path || "unknown";
1546
+ const value = parseFloat(entry.value[1]);
1547
+ if (!results[workflow]) results[workflow] = {};
1548
+ results[workflow][key] = value;
1549
+ }
1550
+ }
1551
+ })
1552
+ );
1553
+
1554
+ const tbody = document.getElementById("workflow-metrics-body");
1555
+ tbody.innerHTML = "";
1556
+
1557
+ Object.entries(results)
1558
+ .sort(([, a], [, b]) => (b.requests || 0) - (a.requests || 0))
1559
+ .forEach(([workflow, metrics]) => {
1560
+ const row = document.createElement("tr");
1561
+ row.className =
1562
+ "hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors duration-200";
1563
+
1564
+ row.innerHTML = `
1565
+ <td class="px-4 py-3 font-medium text-gray-900 dark:text-white whitespace-nowrap">
1566
+ ${workflow}
1567
+ </td>
1568
+ <td class="px-4 py-3 text-right text-gray-700 dark:text-gray-300">
1569
+ ${Math.round(metrics.requests || 0).toLocaleString()}
1570
+ </td>
1571
+ <td class="px-4 py-3 text-right ${
1572
+ metrics.errors > 0
1573
+ ? "text-red-600 dark:text-red-400"
1574
+ : "text-gray-700 dark:text-gray-300"
1575
+ }">
1576
+ ${Math.round(metrics.errors || 0).toLocaleString()}
1577
+ </td>
1578
+ <td class="px-4 py-3 text-right text-gray-700 dark:text-gray-300">
1579
+ ${(metrics.time || 0).toFixed(2)} ms
1580
+ </td>
1581
+ <td class="px-4 py-3 text-right text-gray-700 dark:text-gray-300">
1582
+ ${(metrics.cpu || 0).toFixed(1)} %
1583
+ </td>
1584
+ <td class="px-4 py-3 text-right text-gray-700 dark:text-gray-300">
1585
+ ${(metrics.memory || 0).toFixed(1)}
1586
+ </td>
1587
+ `;
1588
+
1589
+ tbody.appendChild(row);
1590
+ });
1591
+ }
1592
+
1593
+ async function updateNodeTable() {
1594
+ const queries = {
1595
+ requests: "sum(increase(node_total[1m])) by (node_name)",
1596
+ errors: "sum(increase(node_errors_total[1m])) by (node_name)",
1597
+ time: "sum(increase(node_time[1m])) by (node_name)",
1598
+ cpu: "(sum(increase(node_cpu[1m])) by (node_name) / sum(increase(node_total[1m])) by (node_name)) * 100",
1599
+ memory: "sum(increase(node_memory[1m])) by (node_name)",
1600
+ };
1601
+
1602
+ const endpoint = `${prometheusBase}/api/metrics`;
1603
+ const selectedNodes =
1604
+ window.selectedFilters?.node || new Set(["__all__"]);
1605
+ const filterMatcher = buildMatcher("node_name", selectedNodes);
1606
+
1607
+ const results = {};
1608
+
1609
+ await Promise.all(
1610
+ Object.entries(queries).map(async ([key, query]) => {
1611
+ const effectiveQuery = filterMatcher
1612
+ ? query.replace(
1613
+ /node_(total|errors_total|time|cpu|memory)/g,
1614
+ (match) => `${match}{${filterMatcher}}`
1615
+ )
1616
+ : query;
1617
+ const res = await fetch(
1618
+ `${endpoint}?query=${encodeURIComponent(effectiveQuery)}`,
1619
+ {
1620
+ headers: {
1621
+ "x-table": "true",
1622
+ },
1623
+ }
1624
+ );
1625
+ const json = await res.json();
1626
+ if (json.status === "success") {
1627
+ for (const entry of json.data.result) {
1628
+ const workflow =
1629
+ key === "requests"
1630
+ ? entry.metric.node_name || "unknown"
1631
+ : entry.metric.node_name || "unknown";
1632
+ const value = parseFloat(entry.value[1]);
1633
+ if (!results[workflow]) results[workflow] = {};
1634
+ results[workflow][key] = value;
1635
+ }
1636
+ }
1637
+ })
1638
+ );
1639
+
1640
+ const tbody = document.getElementById("nodes-metrics-body");
1641
+ tbody.innerHTML = "";
1642
+
1643
+ Object.entries(results)
1644
+ .sort(([, a], [, b]) => (b.requests || 0) - (a.requests || 0))
1645
+ .forEach(([workflow, metrics]) => {
1646
+ const row = document.createElement("tr");
1647
+ row.className =
1648
+ "hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors duration-200";
1649
+
1650
+ row.innerHTML = `
1651
+ <td class="px-4 py-3 font-medium text-gray-900 dark:text-white whitespace-nowrap">
1652
+ ${workflow}
1653
+ </td>
1654
+ <td class="px-4 py-3 text-right text-gray-700 dark:text-gray-300">
1655
+ ${Math.round(metrics.requests || 0).toLocaleString()}
1656
+ </td>
1657
+ <td class="px-4 py-3 text-right ${
1658
+ metrics.errors > 0
1659
+ ? "text-red-600 dark:text-red-400"
1660
+ : "text-gray-700 dark:text-gray-300"
1661
+ }">
1662
+ ${Math.round(metrics.errors || 0).toLocaleString()}
1663
+ </td>
1664
+ <td class="px-4 py-3 text-right text-gray-700 dark:text-gray-300">
1665
+ ${(metrics.time || 0).toFixed(2)} ms
1666
+ </td>
1667
+ <td class="px-4 py-3 text-right text-gray-700 dark:text-gray-300">
1668
+ ${(metrics.cpu || 0).toFixed(1)} %
1669
+ </td>
1670
+ <td class="px-4 py-3 text-right text-gray-700 dark:text-gray-300">
1671
+ ${(metrics.memory || 0).toFixed(1)}
1672
+ </td>
1673
+ `;
1674
+
1675
+ tbody.appendChild(row);
1676
+ });
1677
+ }
1678
+
1679
+ const thresholdBackgroundPlugin = {
1680
+ id: "thresholdBackground",
1681
+ beforeDraw: (chart) => {
1682
+ const { ctx, chartArea: area, scales } = chart;
1683
+ if (!area || !scales?.y) return;
1684
+
1685
+ const y100 = scales.y.getPixelForValue(100);
1686
+ const y90 = scales.y.getPixelForValue(90);
1687
+ const y70 = scales.y.getPixelForValue(70);
1688
+ const y0 = scales.y.getPixelForValue(0);
1689
+
1690
+ // Red: 90–100%
1691
+ ctx.fillStyle = "#fee2e2";
1692
+ ctx.fillRect(area.left, y100, area.width, y90 - y100);
1693
+
1694
+ // Yellow: 70–90%
1695
+ ctx.fillStyle = "#fef9c3";
1696
+ ctx.fillRect(area.left, y90, area.width, y70 - y90);
1697
+
1698
+ // Green: 0–70%
1699
+ ctx.fillStyle = "#dcfce7";
1700
+ ctx.fillRect(area.left, y70, area.width, y0 - y70);
1701
+ },
1702
+ };
1703
+
1704
+ const thresholdErrorsBackgroundPlugin = {
1705
+ id: "thresholdBackground",
1706
+ beforeDraw: (chart) => {
1707
+ const { ctx, chartArea: area, scales } = chart;
1708
+ if (!area || !scales?.y) return;
1709
+
1710
+ const y100 = scales.y.getPixelForValue(100);
1711
+ const y90 = scales.y.getPixelForValue(10);
1712
+ const y70 = scales.y.getPixelForValue(5);
1713
+ const y0 = scales.y.getPixelForValue(0);
1714
+
1715
+ // Red: 90–100%
1716
+ ctx.fillStyle = "#fee2e2";
1717
+ ctx.fillRect(area.left, y100, area.width, y90 - y100);
1718
+
1719
+ // Yellow: 70–90%
1720
+ ctx.fillStyle = "#fef9c3";
1721
+ ctx.fillRect(area.left, y90, area.width, y70 - y90);
1722
+
1723
+ // Green: 0–70%
1724
+ ctx.fillStyle = "#dcfce7";
1725
+ ctx.fillRect(area.left, y70, area.width, y0 - y70);
1726
+ },
1727
+ };
1728
+
1729
+ function applySectionFilterVisibility() {
1730
+ const selected = window.selectedFilters?.section;
1731
+ if (!selected) return;
1732
+
1733
+ const allVisible = selected.has("__all__");
1734
+
1735
+ document
1736
+ .querySelectorAll("section[data-section-id]")
1737
+ .forEach((section) => {
1738
+ const sectionName = section.getAttribute("data-section-id");
1739
+ const shouldShow = allVisible || selected.has(sectionName);
1740
+ section.style.display = shouldShow ? "" : "none";
1741
+ });
1742
+ }
1743
+
1744
+ function refreshDashboard() {
1745
+ updateAllCharts();
1746
+ updateWorkflowTable();
1747
+ updateNodeTable();
1748
+ }
1749
+
1750
+ function setTheme(theme) {
1751
+ document.documentElement.classList.toggle("dark", theme === "dark");
1752
+ localStorage.setItem("theme", theme);
1753
+ updateDarkIcon(theme);
1754
+ }
1755
+
1756
+ function toggleDarkMode() {
1757
+ const currentTheme = document.documentElement.classList.contains("dark")
1758
+ ? "dark"
1759
+ : "light";
1760
+ const newTheme = currentTheme === "dark" ? "light" : "dark";
1761
+ setTheme(newTheme);
1762
+ }
1763
+
1764
+ function updateDarkIcon(theme) {
1765
+ const iconEl = document.getElementById("darkModeIcon");
1766
+ iconEl.innerHTML = "";
1767
+ const icon = document.createElement("i");
1768
+ icon.setAttribute("data-feather", theme === "dark" ? "sun" : "moon");
1769
+ iconEl.appendChild(icon);
1770
+ feather.replace();
1771
+ }
1772
+
1773
+ // 💡 INIT: Always respect localStorage on first load
1774
+ (function () {
1775
+ const storedTheme = localStorage.getItem("theme");
1776
+
1777
+ if (storedTheme === "dark") {
1778
+ document.documentElement.classList.add("dark");
1779
+ } else if (storedTheme === "light") {
1780
+ document.documentElement.classList.remove("dark");
1781
+ } else {
1782
+ // First visit: use system preference
1783
+ const prefersDark = window.matchMedia(
1784
+ "(prefers-color-scheme: dark)"
1785
+ ).matches;
1786
+ const systemTheme = prefersDark ? "dark" : "light";
1787
+ setTheme(systemTheme);
1788
+ return;
1789
+ }
1790
+
1791
+ updateDarkIcon(storedTheme || "light");
1792
+ })();
1793
+ document.addEventListener("DOMContentLoaded", () => {
1794
+ document.querySelectorAll("canvas").forEach((canvas) => {
1795
+ canvas.ondblclick = async () => {
1796
+ goFullScreen(canvas);
1797
+ };
1798
+ });
1799
+ });
1800
+ function goFullScreen(canvas) {
1801
+ if (canvas.requestFullScreen) canvas.requestFullScreen();
1802
+ else if (canvas.webkitRequestFullScreen)
1803
+ canvas.webkitRequestFullScreen();
1804
+ else if (canvas.mozRequestFullScreen) canvas.mozRequestFullScreen();
1805
+
1806
+ // Get the DPR and size of the canvas
1807
+ const dpr = window.devicePixelRatio;
1808
+ const rect = canvas.getBoundingClientRect();
1809
+
1810
+ // Set the "actual" size of the canvas
1811
+ canvas.width = rect.width * 2 * dpr;
1812
+ canvas.height = rect.height * 2 * dpr;
1813
+
1814
+ // Scale the context to ensure correct drawing operations
1815
+ ctx.scale(dpr, dpr);
1816
+
1817
+ // Set the "drawn" size of the canvas
1818
+ canvas.style.width = `${canvas.width}px`;
1819
+ canvas.style.height = `${canvas.height}px`;
1820
+ }
1821
+
1822
+ document.addEventListener("DOMContentLoaded", () => {
1823
+ const containers = document.querySelectorAll(
1824
+ "[data-draggable-section]"
1825
+ );
1826
+
1827
+ containers.forEach((container) => {
1828
+ const sectionId = container.getAttribute("data-draggable-section");
1829
+ let draggedItem = null;
1830
+
1831
+ // Restaurar orden
1832
+ const savedOrder = JSON.parse(
1833
+ localStorage.getItem(`dashboard-order-${sectionId}`)
1834
+ );
1835
+ if (savedOrder) {
1836
+ savedOrder.forEach((id) => {
1837
+ const item = container.querySelector(`[data-id="${id}"]`);
1838
+ if (item) container.appendChild(item);
1839
+ });
1840
+ }
1841
+
1842
+ // Guardar orden
1843
+ const saveOrder = () => {
1844
+ const order = Array.from(container.children).map((item) =>
1845
+ item.getAttribute("data-id")
1846
+ );
1847
+ localStorage.setItem(
1848
+ `dashboard-order-${sectionId}`,
1849
+ JSON.stringify(order)
1850
+ );
1851
+ };
1852
+
1853
+ // Eventos drag and drop
1854
+ container.addEventListener("dragstart", (e) => {
1855
+ if (e.target.matches("[draggable]")) {
1856
+ draggedItem = e.target;
1857
+ e.dataTransfer.effectAllowed = "move";
1858
+ }
1859
+ });
1860
+
1861
+ container.addEventListener("dragover", (e) => {
1862
+ e.preventDefault();
1863
+ const target = e.target.closest("[draggable]");
1864
+ if (target && target !== draggedItem) {
1865
+ const rect = target.getBoundingClientRect();
1866
+ const next = e.clientY - rect.top > rect.height / 2;
1867
+ container.insertBefore(
1868
+ draggedItem,
1869
+ next ? target.nextSibling : target
1870
+ );
1871
+ }
1872
+ });
1873
+
1874
+ container.addEventListener("drop", (e) => {
1875
+ e.preventDefault();
1876
+ saveOrder();
1877
+ });
1878
+ });
1879
+ });
1880
+
1881
+ function persistSelectedFilters() {
1882
+ const filtersToSave = {};
1883
+ for (const key in window.selectedFilters) {
1884
+ filtersToSave[key] = Array.from(window.selectedFilters[key]);
1885
+ }
1886
+ localStorage.setItem(
1887
+ "dashboard:selectedFilters",
1888
+ JSON.stringify(filtersToSave)
1889
+ );
1890
+ }
1891
+
1892
+ async function loadFilterOptionsV2() {
1893
+ const filters = [
1894
+ {
1895
+ id: "workflow",
1896
+ url: "http://localhost:4040/api/metrics",
1897
+ label: "workflow_path",
1898
+ },
1899
+ {
1900
+ id: "node",
1901
+ url: "http://localhost:4040/api/metrics",
1902
+ label: "node_name",
1903
+ },
1904
+ {
1905
+ id: "section",
1906
+ static: true,
1907
+ values: [
1908
+ "System Overview",
1909
+ "Workflow Metrics",
1910
+ "Node Metrics",
1911
+ "Workflow Table",
1912
+ "Node Table",
1913
+ "System Logs",
1914
+ "Error Logs",
1915
+ ],
1916
+ },
1917
+ ];
1918
+
1919
+ window.selectedFilters = {
1920
+ workflow: new Set(["__all__"]),
1921
+ node: new Set(["__all__"]),
1922
+ };
1923
+
1924
+ const savedFilters = JSON.parse(
1925
+ localStorage.getItem("dashboard:selectedFilters") || "{}"
1926
+ );
1927
+
1928
+ for (const filter of filters) {
1929
+ let data;
1930
+
1931
+ if (filter.static) {
1932
+ data = { data: filter.values };
1933
+ } else {
1934
+ const response = await fetch(filter.url, {
1935
+ headers: {
1936
+ "x-label": filter.label,
1937
+ },
1938
+ });
1939
+ data = await response.json();
1940
+ }
1941
+
1942
+ const optionsContainer = document.getElementById(
1943
+ `${filter.id}-filter-options`
1944
+ );
1945
+ const placeholder = document.getElementById(
1946
+ `${filter.id}-filter-placeholder`
1947
+ );
1948
+ const dropdown = document.getElementById(
1949
+ `${filter.id}-filter-dropdown`
1950
+ );
1951
+ const trigger = document.getElementById(
1952
+ `${filter.id}-filter-trigger`
1953
+ );
1954
+ const selectedLabel = dropdown.querySelector("div.text-xs");
1955
+
1956
+ const saved = savedFilters[filter.id];
1957
+ window.selectedFilters[filter.id] = saved
1958
+ ? new Set(saved)
1959
+ : new Set(["__all__"]);
1960
+
1961
+ trigger.addEventListener("click", () => {
1962
+ dropdown.classList.toggle("hidden");
1963
+ const chevron = document.getElementById(
1964
+ `${filter.id}-filter-chevron`
1965
+ );
1966
+ chevron.classList.toggle("rotate-180");
1967
+ });
1968
+
1969
+ const allOption = document.createElement("label");
1970
+ allOption.className =
1971
+ "flex items-center px-3 py-1.5 text-sm hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-800 dark:text-gray-200 cursor-pointer";
1972
+ allOption.innerHTML = `
1973
+ <input type="checkbox" id="${filter.id}-opt-all" value="__all__" class="mr-2 text-blue-500 bg-gray-700 border-gray-600 rounded focus:ring-blue-500">
1974
+ All
1975
+ `;
1976
+ const allInput = allOption.querySelector("input");
1977
+ // Event handler for "All" checkbox item
1978
+ allInput.addEventListener("change", () => {
1979
+ const checkboxes = optionsContainer.querySelectorAll(
1980
+ "input[type='checkbox']"
1981
+ );
1982
+ window.selectedFilters[filter.id] = new Set();
1983
+
1984
+ checkboxes.forEach((cb) => {
1985
+ cb.checked = allInput.checked;
1986
+ if (allInput.checked && cb.value !== "__all__") {
1987
+ window.selectedFilters[filter.id].add(cb.value);
1988
+ }
1989
+ });
1990
+
1991
+ if (allInput.checked) {
1992
+ window.selectedFilters[filter.id] = new Set(["__all__"]);
1993
+ placeholder.textContent = "All";
1994
+ selectedLabel.textContent = `Selected (All)`;
1995
+ } else {
1996
+ placeholder.textContent = "Enter variable value";
1997
+ selectedLabel.textContent = `Selected (0)`;
1998
+ }
1999
+
2000
+ persistSelectedFilters();
2001
+ refreshDashboard();
2002
+ applySectionFilterVisibility();
2003
+ });
2004
+
2005
+ optionsContainer.appendChild(allOption);
2006
+ const validItems = data.data.filter(
2007
+ (item) => typeof item === "string" && item.trim() !== "" && item !== "undefined"
2008
+ );
2009
+ validItems.forEach((item) => {
2010
+ const id = `${filter.id}-opt-${item}`;
2011
+ const option = document.createElement("label");
2012
+ option.className =
2013
+ "flex items-center px-3 py-1.5 text-sm hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-800 dark:text-gray-200 cursor-pointer";
2014
+
2015
+ option.innerHTML = `
2016
+ <input type="checkbox" id="${id}" value="${item}" class="mr-2 text-blue-500 bg-gray-700 border-gray-600 rounded focus:ring-blue-500">
2017
+ ${item}
2018
+ `;
2019
+
2020
+ const input = option.querySelector("input");
2021
+ input.addEventListener("change", () => {
2022
+ requestAnimationFrame(() => {
2023
+ const checkboxes = optionsContainer.querySelectorAll(
2024
+ "input[type='checkbox']:not([value='__all__'])"
2025
+ );
2026
+ const checkedBoxes = optionsContainer.querySelectorAll(
2027
+ "input[type='checkbox']:not([value='__all__']):checked"
2028
+ );
2029
+
2030
+ // Limpiar y reconstruir el Set según el estado actual
2031
+ window.selectedFilters[filter.id] = new Set();
2032
+ checkedBoxes.forEach((cb) =>
2033
+ window.selectedFilters[filter.id].add(cb.value)
2034
+ );
2035
+
2036
+ if (checkedBoxes.length === checkboxes.length) {
2037
+ allInput.checked = true;
2038
+ window.selectedFilters[filter.id] = new Set(["__all__"]);
2039
+ placeholder.textContent = "All";
2040
+ selectedLabel.textContent = `Selected (All)`;
2041
+ } else {
2042
+ allInput.checked = false;
2043
+ const count = checkedBoxes.length;
2044
+ placeholder.textContent =
2045
+ count > 0 ? `${count} selected` : "Enter variable value";
2046
+ selectedLabel.textContent = `Selected (${count})`;
2047
+ }
2048
+
2049
+ persistSelectedFilters();
2050
+ refreshDashboard();
2051
+ applySectionFilterVisibility();
2052
+ });
2053
+ });
2054
+
2055
+ optionsContainer.appendChild(option);
2056
+ });
2057
+
2058
+ // Preseleccionar todos al cargar
2059
+ requestAnimationFrame(() => {
2060
+ const allSelected =
2061
+ window.selectedFilters[filter.id].has("__all__");
2062
+ const checkboxes = optionsContainer.querySelectorAll(
2063
+ "input[type='checkbox']:not([value='__all__'])"
2064
+ );
2065
+
2066
+ let selectedCount = 0;
2067
+ checkboxes.forEach((cb) => {
2068
+ cb.checked =
2069
+ allSelected || window.selectedFilters[filter.id].has(cb.value);
2070
+ if (cb.checked) selectedCount++;
2071
+ });
2072
+
2073
+ allInput.checked = allSelected;
2074
+ placeholder.textContent = allSelected
2075
+ ? "All"
2076
+ : selectedCount > 0
2077
+ ? `${selectedCount} selected`
2078
+ : "Enter variable value";
2079
+ selectedLabel.textContent = allSelected
2080
+ ? `Selected (All)`
2081
+ : `Selected (${selectedCount})`;
2082
+
2083
+ refreshDashboard();
2084
+ applySectionFilterVisibility();
2085
+ });
2086
+ }
2087
+ }
2088
+
2089
+ document.addEventListener("DOMContentLoaded", async () => {
2090
+ await loadFilterOptionsV2(); // Espera a que los filtros estén cargados
2091
+ applySectionFilterVisibility();
2092
+
2093
+ setInterval(() => {
2094
+ updateAllCharts();
2095
+ updateLokiLogs();
2096
+ updateLokiErrors();
2097
+ updateWorkflowTable();
2098
+ updateNodeTable();
2099
+ }, 5000);
2100
+
2101
+ // Cerrar dropdowns al hacer clic fuera
2102
+ document.addEventListener("click", (event) => {
2103
+ const dropdowns = ["workflow", "node", "section"];
2104
+ dropdowns.forEach((id) => {
2105
+ const trigger = document.getElementById(`${id}-filter-trigger`);
2106
+ const dropdown = document.getElementById(`${id}-filter-dropdown`);
2107
+ const chevron = document.getElementById(`${id}-filter-chevron`);
2108
+
2109
+ if (!trigger || !dropdown || !chevron) return;
2110
+
2111
+ const clickedOutside =
2112
+ !trigger.contains(event.target) &&
2113
+ !dropdown.contains(event.target);
2114
+
2115
+ if (clickedOutside) {
2116
+ dropdown.classList.add("hidden");
2117
+ chevron.classList.remove("rotate-180");
2118
+ }
2119
+ });
2120
+ });
2121
+ });
2122
+ </script>
2123
+ </body>
2124
+ </html>