@jagilber-org/index-server 1.19.1

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 (360) hide show
  1. package/CHANGELOG.md +1218 -0
  2. package/CODE_OF_CONDUCT.md +49 -0
  3. package/CONTRIBUTING.md +75 -0
  4. package/LICENSE +21 -0
  5. package/README.md +523 -0
  6. package/SECURITY.md +50 -0
  7. package/dist/config/configUtils.d.ts +11 -0
  8. package/dist/config/configUtils.js +87 -0
  9. package/dist/config/dashboardConfig.d.ts +45 -0
  10. package/dist/config/dashboardConfig.js +63 -0
  11. package/dist/config/defaultValues.d.ts +61 -0
  12. package/dist/config/defaultValues.js +70 -0
  13. package/dist/config/dirConstants.d.ts +17 -0
  14. package/dist/config/dirConstants.js +28 -0
  15. package/dist/config/featureConfig.d.ts +61 -0
  16. package/dist/config/featureConfig.js +121 -0
  17. package/dist/config/runtimeConfig.d.ts +145 -0
  18. package/dist/config/runtimeConfig.js +334 -0
  19. package/dist/config/serverConfig.d.ts +90 -0
  20. package/dist/config/serverConfig.js +164 -0
  21. package/dist/dashboard/analytics/AnalyticsEngine.d.ts +142 -0
  22. package/dist/dashboard/analytics/AnalyticsEngine.js +373 -0
  23. package/dist/dashboard/analytics/BusinessIntelligence.d.ts +187 -0
  24. package/dist/dashboard/analytics/BusinessIntelligence.js +594 -0
  25. package/dist/dashboard/client/admin.html +2150 -0
  26. package/dist/dashboard/client/chunks/mermaid-layout-elk.esm.min/chunk-SP2CHFBE.mjs +1 -0
  27. package/dist/dashboard/client/chunks/mermaid-layout-elk.esm.min/render-T6MDALS3.mjs +27 -0
  28. package/dist/dashboard/client/css/admin.css +1466 -0
  29. package/dist/dashboard/client/js/admin.boot.js +359 -0
  30. package/dist/dashboard/client/js/admin.config.js +196 -0
  31. package/dist/dashboard/client/js/admin.embeddings.js +425 -0
  32. package/dist/dashboard/client/js/admin.graph.js +583 -0
  33. package/dist/dashboard/client/js/admin.instances.js +120 -0
  34. package/dist/dashboard/client/js/admin.instructions.js +552 -0
  35. package/dist/dashboard/client/js/admin.logs.js +113 -0
  36. package/dist/dashboard/client/js/admin.maintenance.js +354 -0
  37. package/dist/dashboard/client/js/admin.messaging.js +635 -0
  38. package/dist/dashboard/client/js/admin.monitor.js +181 -0
  39. package/dist/dashboard/client/js/admin.overview.js +221 -0
  40. package/dist/dashboard/client/js/admin.performance.js +61 -0
  41. package/dist/dashboard/client/js/admin.sessions.js +293 -0
  42. package/dist/dashboard/client/js/admin.sqlite.js +366 -0
  43. package/dist/dashboard/client/js/admin.utils.js +49 -0
  44. package/dist/dashboard/client/js/chart.umd.js +14 -0
  45. package/dist/dashboard/client/js/elk.bundled.js +6696 -0
  46. package/dist/dashboard/client/js/marked.umd.js +74 -0
  47. package/dist/dashboard/client/js/mermaid.min.js +3022 -0
  48. package/dist/dashboard/client/mermaid-layout-elk.esm.min.mjs +1 -0
  49. package/dist/dashboard/export/DataExporter.d.ts +169 -0
  50. package/dist/dashboard/export/DataExporter.js +737 -0
  51. package/dist/dashboard/export/exporters/csvExporter.d.ts +11 -0
  52. package/dist/dashboard/export/exporters/csvExporter.js +46 -0
  53. package/dist/dashboard/export/exporters/exportTypes.d.ts +89 -0
  54. package/dist/dashboard/export/exporters/exportTypes.js +5 -0
  55. package/dist/dashboard/export/exporters/jsonExporter.d.ts +7 -0
  56. package/dist/dashboard/export/exporters/jsonExporter.js +22 -0
  57. package/dist/dashboard/export/exporters/xmlExporter.d.ts +17 -0
  58. package/dist/dashboard/export/exporters/xmlExporter.js +175 -0
  59. package/dist/dashboard/integration/APIIntegration.d.ts +41 -0
  60. package/dist/dashboard/integration/APIIntegration.js +95 -0
  61. package/dist/dashboard/security/SecurityMonitor.d.ts +167 -0
  62. package/dist/dashboard/security/SecurityMonitor.js +559 -0
  63. package/dist/dashboard/server/AdminPanel.d.ts +183 -0
  64. package/dist/dashboard/server/AdminPanel.js +792 -0
  65. package/dist/dashboard/server/AdminPanelConfig.d.ts +42 -0
  66. package/dist/dashboard/server/AdminPanelConfig.js +80 -0
  67. package/dist/dashboard/server/AdminPanelState.d.ts +47 -0
  68. package/dist/dashboard/server/AdminPanelState.js +214 -0
  69. package/dist/dashboard/server/ApiRoutes.d.ts +17 -0
  70. package/dist/dashboard/server/ApiRoutes.js +149 -0
  71. package/dist/dashboard/server/DashboardServer.d.ts +49 -0
  72. package/dist/dashboard/server/DashboardServer.js +159 -0
  73. package/dist/dashboard/server/FileMetricsStorage.d.ts +49 -0
  74. package/dist/dashboard/server/FileMetricsStorage.js +195 -0
  75. package/dist/dashboard/server/HttpTransport.d.ts +23 -0
  76. package/dist/dashboard/server/HttpTransport.js +116 -0
  77. package/dist/dashboard/server/InstanceManager.d.ts +53 -0
  78. package/dist/dashboard/server/InstanceManager.js +284 -0
  79. package/dist/dashboard/server/KnowledgeStore.d.ts +35 -0
  80. package/dist/dashboard/server/KnowledgeStore.js +105 -0
  81. package/dist/dashboard/server/LeaderElection.d.ts +81 -0
  82. package/dist/dashboard/server/LeaderElection.js +268 -0
  83. package/dist/dashboard/server/MetricsCollector.d.ts +200 -0
  84. package/dist/dashboard/server/MetricsCollector.js +803 -0
  85. package/dist/dashboard/server/SessionPersistenceManager.d.ts +88 -0
  86. package/dist/dashboard/server/SessionPersistenceManager.js +457 -0
  87. package/dist/dashboard/server/ThinClient.d.ts +64 -0
  88. package/dist/dashboard/server/ThinClient.js +237 -0
  89. package/dist/dashboard/server/WebSocketManager.d.ts +161 -0
  90. package/dist/dashboard/server/WebSocketManager.js +463 -0
  91. package/dist/dashboard/server/httpLifecycle.d.ts +17 -0
  92. package/dist/dashboard/server/httpLifecycle.js +35 -0
  93. package/dist/dashboard/server/legacyDashboardHtml.d.ts +9 -0
  94. package/dist/dashboard/server/legacyDashboardHtml.js +618 -0
  95. package/dist/dashboard/server/legacyDashboardStyles.d.ts +5 -0
  96. package/dist/dashboard/server/legacyDashboardStyles.js +490 -0
  97. package/dist/dashboard/server/metricsAggregation.d.ts +252 -0
  98. package/dist/dashboard/server/metricsAggregation.js +206 -0
  99. package/dist/dashboard/server/metricsSerializer.d.ts +25 -0
  100. package/dist/dashboard/server/metricsSerializer.js +195 -0
  101. package/dist/dashboard/server/routes/admin.routes.d.ts +16 -0
  102. package/dist/dashboard/server/routes/admin.routes.js +596 -0
  103. package/dist/dashboard/server/routes/alerts.routes.d.ts +7 -0
  104. package/dist/dashboard/server/routes/alerts.routes.js +93 -0
  105. package/dist/dashboard/server/routes/api.feedback.routes.d.ts +73 -0
  106. package/dist/dashboard/server/routes/api.feedback.routes.js +171 -0
  107. package/dist/dashboard/server/routes/api.instructions.routes.d.ts +101 -0
  108. package/dist/dashboard/server/routes/api.instructions.routes.js +213 -0
  109. package/dist/dashboard/server/routes/api.usage.routes.d.ts +57 -0
  110. package/dist/dashboard/server/routes/api.usage.routes.js +374 -0
  111. package/dist/dashboard/server/routes/embeddings.routes.d.ts +6 -0
  112. package/dist/dashboard/server/routes/embeddings.routes.js +246 -0
  113. package/dist/dashboard/server/routes/graph.routes.d.ts +6 -0
  114. package/dist/dashboard/server/routes/graph.routes.js +280 -0
  115. package/dist/dashboard/server/routes/index.d.ts +38 -0
  116. package/dist/dashboard/server/routes/index.js +194 -0
  117. package/dist/dashboard/server/routes/instances.routes.d.ts +6 -0
  118. package/dist/dashboard/server/routes/instances.routes.js +35 -0
  119. package/dist/dashboard/server/routes/instructions.routes.d.ts +8 -0
  120. package/dist/dashboard/server/routes/instructions.routes.js +336 -0
  121. package/dist/dashboard/server/routes/knowledge.routes.d.ts +6 -0
  122. package/dist/dashboard/server/routes/knowledge.routes.js +82 -0
  123. package/dist/dashboard/server/routes/logs.routes.d.ts +6 -0
  124. package/dist/dashboard/server/routes/logs.routes.js +164 -0
  125. package/dist/dashboard/server/routes/messaging.routes.d.ts +16 -0
  126. package/dist/dashboard/server/routes/messaging.routes.js +293 -0
  127. package/dist/dashboard/server/routes/metrics.routes.d.ts +10 -0
  128. package/dist/dashboard/server/routes/metrics.routes.js +346 -0
  129. package/dist/dashboard/server/routes/scripts.routes.d.ts +9 -0
  130. package/dist/dashboard/server/routes/scripts.routes.js +84 -0
  131. package/dist/dashboard/server/routes/sqlite.routes.d.ts +9 -0
  132. package/dist/dashboard/server/routes/sqlite.routes.js +569 -0
  133. package/dist/dashboard/server/routes/status.routes.d.ts +7 -0
  134. package/dist/dashboard/server/routes/status.routes.js +183 -0
  135. package/dist/dashboard/server/routes/synthetic.routes.d.ts +7 -0
  136. package/dist/dashboard/server/routes/synthetic.routes.js +195 -0
  137. package/dist/dashboard/server/routes/tools.routes.d.ts +6 -0
  138. package/dist/dashboard/server/routes/tools.routes.js +46 -0
  139. package/dist/dashboard/server/routes/usage.routes.d.ts +6 -0
  140. package/dist/dashboard/server/routes/usage.routes.js +25 -0
  141. package/dist/dashboard/server/wsInit.d.ts +16 -0
  142. package/dist/dashboard/server/wsInit.js +35 -0
  143. package/dist/externalClientLib.d.ts +1 -0
  144. package/dist/externalClientLib.js +2 -0
  145. package/dist/minimal/index.d.ts +1 -0
  146. package/dist/minimal/index.js +140 -0
  147. package/dist/models/SessionPersistence.d.ts +115 -0
  148. package/dist/models/SessionPersistence.js +66 -0
  149. package/dist/models/instruction.d.ts +45 -0
  150. package/dist/models/instruction.js +2 -0
  151. package/dist/perf/benchmark.d.ts +1 -0
  152. package/dist/perf/benchmark.js +50 -0
  153. package/dist/portableClientWrapper.d.ts +1 -0
  154. package/dist/portableClientWrapper.js +2 -0
  155. package/dist/schemas/index.d.ts +128 -0
  156. package/dist/schemas/index.js +371 -0
  157. package/dist/scripts/runPerformanceBaseline.d.ts +1 -0
  158. package/dist/scripts/runPerformanceBaseline.js +17 -0
  159. package/dist/server/handshakeManager.d.ts +25 -0
  160. package/dist/server/handshakeManager.js +472 -0
  161. package/dist/server/index-server.d.ts +56 -0
  162. package/dist/server/index-server.js +822 -0
  163. package/dist/server/registry.d.ts +44 -0
  164. package/dist/server/registry.js +236 -0
  165. package/dist/server/sdkServer.d.ts +8 -0
  166. package/dist/server/sdkServer.js +299 -0
  167. package/dist/server/shutdownGuard.d.ts +41 -0
  168. package/dist/server/shutdownGuard.js +52 -0
  169. package/dist/server/thin-client.d.ts +22 -0
  170. package/dist/server/thin-client.js +111 -0
  171. package/dist/server/transport.d.ts +41 -0
  172. package/dist/server/transport.js +312 -0
  173. package/dist/server/transportFactory.d.ts +21 -0
  174. package/dist/server/transportFactory.js +429 -0
  175. package/dist/services/atomicFs.d.ts +22 -0
  176. package/dist/services/atomicFs.js +103 -0
  177. package/dist/services/auditLog.d.ts +38 -0
  178. package/dist/services/auditLog.js +142 -0
  179. package/dist/services/autoBackup.d.ts +14 -0
  180. package/dist/services/autoBackup.js +171 -0
  181. package/dist/services/autoSplit.d.ts +32 -0
  182. package/dist/services/autoSplit.js +113 -0
  183. package/dist/services/backupZip.d.ts +25 -0
  184. package/dist/services/backupZip.js +110 -0
  185. package/dist/services/bootstrapGating.d.ts +123 -0
  186. package/dist/services/bootstrapGating.js +221 -0
  187. package/dist/services/canonical.d.ts +23 -0
  188. package/dist/services/canonical.js +65 -0
  189. package/dist/services/categoryRules.d.ts +7 -0
  190. package/dist/services/categoryRules.js +37 -0
  191. package/dist/services/classificationService.d.ts +42 -0
  192. package/dist/services/classificationService.js +168 -0
  193. package/dist/services/embeddingService.d.ts +62 -0
  194. package/dist/services/embeddingService.js +259 -0
  195. package/dist/services/errors.d.ts +22 -0
  196. package/dist/services/errors.js +31 -0
  197. package/dist/services/featureFlags.d.ts +25 -0
  198. package/dist/services/featureFlags.js +89 -0
  199. package/dist/services/features.d.ts +13 -0
  200. package/dist/services/features.js +35 -0
  201. package/dist/services/handlers/instructions.add.d.ts +1 -0
  202. package/dist/services/handlers/instructions.add.js +496 -0
  203. package/dist/services/handlers/instructions.groom.d.ts +1 -0
  204. package/dist/services/handlers/instructions.groom.js +523 -0
  205. package/dist/services/handlers/instructions.import.d.ts +1 -0
  206. package/dist/services/handlers/instructions.import.js +173 -0
  207. package/dist/services/handlers/instructions.patch.d.ts +1 -0
  208. package/dist/services/handlers/instructions.patch.js +167 -0
  209. package/dist/services/handlers/instructions.query.d.ts +163 -0
  210. package/dist/services/handlers/instructions.query.js +522 -0
  211. package/dist/services/handlers/instructions.reload.d.ts +1 -0
  212. package/dist/services/handlers/instructions.reload.js +13 -0
  213. package/dist/services/handlers/instructions.remove.d.ts +1 -0
  214. package/dist/services/handlers/instructions.remove.js +118 -0
  215. package/dist/services/handlers/instructions.shared.d.ts +31 -0
  216. package/dist/services/handlers/instructions.shared.js +124 -0
  217. package/dist/services/handlers.activation.d.ts +1 -0
  218. package/dist/services/handlers.activation.js +203 -0
  219. package/dist/services/handlers.bootstrap.d.ts +1 -0
  220. package/dist/services/handlers.bootstrap.js +38 -0
  221. package/dist/services/handlers.dashboardConfig.d.ts +34 -0
  222. package/dist/services/handlers.dashboardConfig.js +108 -0
  223. package/dist/services/handlers.diagnostics.d.ts +1 -0
  224. package/dist/services/handlers.diagnostics.js +64 -0
  225. package/dist/services/handlers.feedback.d.ts +15 -0
  226. package/dist/services/handlers.feedback.js +378 -0
  227. package/dist/services/handlers.gates.d.ts +1 -0
  228. package/dist/services/handlers.gates.js +46 -0
  229. package/dist/services/handlers.graph.d.ts +53 -0
  230. package/dist/services/handlers.graph.js +231 -0
  231. package/dist/services/handlers.help.d.ts +1 -0
  232. package/dist/services/handlers.help.js +119 -0
  233. package/dist/services/handlers.instructionSchema.d.ts +1 -0
  234. package/dist/services/handlers.instructionSchema.js +227 -0
  235. package/dist/services/handlers.instructions.d.ts +8 -0
  236. package/dist/services/handlers.instructions.js +14 -0
  237. package/dist/services/handlers.instructionsDiagnostics.d.ts +1 -0
  238. package/dist/services/handlers.instructionsDiagnostics.js +14 -0
  239. package/dist/services/handlers.integrity.d.ts +1 -0
  240. package/dist/services/handlers.integrity.js +35 -0
  241. package/dist/services/handlers.manifest.d.ts +1 -0
  242. package/dist/services/handlers.manifest.js +24 -0
  243. package/dist/services/handlers.messaging.d.ts +12 -0
  244. package/dist/services/handlers.messaging.js +203 -0
  245. package/dist/services/handlers.metrics.d.ts +1 -0
  246. package/dist/services/handlers.metrics.js +43 -0
  247. package/dist/services/handlers.promote.d.ts +1 -0
  248. package/dist/services/handlers.promote.js +306 -0
  249. package/dist/services/handlers.prompt.d.ts +1 -0
  250. package/dist/services/handlers.prompt.js +7 -0
  251. package/dist/services/handlers.search.d.ts +69 -0
  252. package/dist/services/handlers.search.js +645 -0
  253. package/dist/services/handlers.testPrimitive.d.ts +1 -0
  254. package/dist/services/handlers.testPrimitive.js +5 -0
  255. package/dist/services/handlers.trace.d.ts +1 -0
  256. package/dist/services/handlers.trace.js +31 -0
  257. package/dist/services/handlers.usage.d.ts +1 -0
  258. package/dist/services/handlers.usage.js +11 -0
  259. package/dist/services/hotScore.d.ts +137 -0
  260. package/dist/services/hotScore.js +244 -0
  261. package/dist/services/indexContext.d.ts +117 -0
  262. package/dist/services/indexContext.js +968 -0
  263. package/dist/services/indexLoader.d.ts +44 -0
  264. package/dist/services/indexLoader.js +921 -0
  265. package/dist/services/indexRepository.d.ts +32 -0
  266. package/dist/services/indexRepository.js +71 -0
  267. package/dist/services/indexingService.d.ts +1 -0
  268. package/dist/services/indexingService.js +2 -0
  269. package/dist/services/instructions.dispatcher.d.ts +1 -0
  270. package/dist/services/instructions.dispatcher.js +231 -0
  271. package/dist/services/logPrefix.d.ts +1 -0
  272. package/dist/services/logPrefix.js +30 -0
  273. package/dist/services/logger.d.ts +52 -0
  274. package/dist/services/logger.js +268 -0
  275. package/dist/services/manifestManager.d.ts +82 -0
  276. package/dist/services/manifestManager.js +200 -0
  277. package/dist/services/messaging/agentMailbox.d.ts +60 -0
  278. package/dist/services/messaging/agentMailbox.js +353 -0
  279. package/dist/services/messaging/messagingPersistence.d.ts +20 -0
  280. package/dist/services/messaging/messagingPersistence.js +111 -0
  281. package/dist/services/messaging/messagingTypes.d.ts +150 -0
  282. package/dist/services/messaging/messagingTypes.js +66 -0
  283. package/dist/services/ownershipService.d.ts +1 -0
  284. package/dist/services/ownershipService.js +38 -0
  285. package/dist/services/performanceBaseline.d.ts +19 -0
  286. package/dist/services/performanceBaseline.js +210 -0
  287. package/dist/services/preflight.d.ts +12 -0
  288. package/dist/services/preflight.js +79 -0
  289. package/dist/services/promptReviewService.d.ts +44 -0
  290. package/dist/services/promptReviewService.js +101 -0
  291. package/dist/services/responseEnvelope.d.ts +6 -0
  292. package/dist/services/responseEnvelope.js +25 -0
  293. package/dist/services/seedBootstrap.d.ts +34 -0
  294. package/dist/services/seedBootstrap.js +427 -0
  295. package/dist/services/storage/factory.d.ts +17 -0
  296. package/dist/services/storage/factory.js +35 -0
  297. package/dist/services/storage/hashUtils.d.ts +11 -0
  298. package/dist/services/storage/hashUtils.js +35 -0
  299. package/dist/services/storage/index.d.ts +12 -0
  300. package/dist/services/storage/index.js +18 -0
  301. package/dist/services/storage/jsonFileStore.d.ts +32 -0
  302. package/dist/services/storage/jsonFileStore.js +241 -0
  303. package/dist/services/storage/migrationEngine.d.ts +35 -0
  304. package/dist/services/storage/migrationEngine.js +93 -0
  305. package/dist/services/storage/sqliteMessageStore.d.ts +53 -0
  306. package/dist/services/storage/sqliteMessageStore.js +146 -0
  307. package/dist/services/storage/sqliteSchema.d.ts +12 -0
  308. package/dist/services/storage/sqliteSchema.js +122 -0
  309. package/dist/services/storage/sqliteStore.d.ts +41 -0
  310. package/dist/services/storage/sqliteStore.js +339 -0
  311. package/dist/services/storage/sqliteUsageStore.d.ts +35 -0
  312. package/dist/services/storage/sqliteUsageStore.js +94 -0
  313. package/dist/services/storage/types.d.ts +171 -0
  314. package/dist/services/storage/types.js +12 -0
  315. package/dist/services/toolHandlers.d.ts +23 -0
  316. package/dist/services/toolHandlers.js +50 -0
  317. package/dist/services/toolRegistry.d.ts +20 -0
  318. package/dist/services/toolRegistry.js +490 -0
  319. package/dist/services/toolRegistry.zod.d.ts +10 -0
  320. package/dist/services/toolRegistry.zod.js +323 -0
  321. package/dist/services/tracing.d.ts +26 -0
  322. package/dist/services/tracing.js +260 -0
  323. package/dist/services/usageBuckets.d.ts +161 -0
  324. package/dist/services/usageBuckets.js +364 -0
  325. package/dist/services/validationService.d.ts +38 -0
  326. package/dist/services/validationService.js +125 -0
  327. package/dist/utils/BufferRing.d.ts +203 -0
  328. package/dist/utils/BufferRing.js +551 -0
  329. package/dist/utils/BufferRingExamples.d.ts +55 -0
  330. package/dist/utils/BufferRingExamples.js +188 -0
  331. package/dist/utils/envUtils.d.ts +42 -0
  332. package/dist/utils/envUtils.js +80 -0
  333. package/dist/utils/memoryMonitor.d.ts +83 -0
  334. package/dist/utils/memoryMonitor.js +275 -0
  335. package/dist/versioning/schemaVersion.d.ts +6 -0
  336. package/dist/versioning/schemaVersion.js +93 -0
  337. package/package.json +134 -0
  338. package/schemas/README.md +13 -0
  339. package/schemas/feedback-entry.schema.json +27 -0
  340. package/schemas/graph-export-v2.schema.json +60 -0
  341. package/schemas/index-server.code-schema.json +38477 -0
  342. package/schemas/instruction.schema.json +262 -0
  343. package/schemas/json-schema/SessionPersistence-persisted-admin-session.schema.json +54 -0
  344. package/schemas/json-schema/SessionPersistence-persisted-session-history-entry.schema.json +51 -0
  345. package/schemas/json-schema/SessionPersistence-persisted-web-socket-connection.schema.json +54 -0
  346. package/schemas/json-schema/SessionPersistence-session-persistence-config.schema.json +110 -0
  347. package/schemas/json-schema/SessionPersistence-session-persistence-data.schema.json +229 -0
  348. package/schemas/json-schema/SessionPersistence-session-persistence-manifest.schema.json +109 -0
  349. package/schemas/json-schema/SessionPersistence-session-persistence-metadata.schema.json +55 -0
  350. package/schemas/json-schema/instruction-audience-scope.schema.json +14 -0
  351. package/schemas/json-schema/instruction-content-type.schema.json +17 -0
  352. package/schemas/json-schema/instruction-instruction-entry.schema.json +206 -0
  353. package/schemas/json-schema/instruction-requirement-level.schema.json +16 -0
  354. package/schemas/manifest.json +78 -0
  355. package/schemas/manifest.schema.json +33 -0
  356. package/schemas/usage-batch.schema.json +16 -0
  357. package/schemas/usage-buckets.schema.json +30 -0
  358. package/schemas/usage-event.schema.json +17 -0
  359. package/scripts/copy-dashboard-assets.mjs +170 -0
  360. package/scripts/setup-hooks.cjs +28 -0
@@ -0,0 +1,2150 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta name="dashboard-build-version" content="1.19.1-e48e1b0d">
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Index Server Admin</title>
8
+ <link rel="stylesheet" href="css/admin.css?v=1.19.1-e48e1b0d">
9
+ <script defer src="js/admin.utils.js?v=1.19.1-e48e1b0d"></script>
10
+ <script defer src="js/admin.overview.js?v=1.19.1-e48e1b0d"></script>
11
+ <script defer src="js/admin.sessions.js?v=1.19.1-e48e1b0d"></script>
12
+ <script defer src="js/admin.monitor.js?v=1.19.1-e48e1b0d"></script>
13
+ <script defer src="js/admin.graph.js?v=1.19.1-e48e1b0d"></script>
14
+ <script defer src="js/marked.umd.js"></script>
15
+ <script defer src="js/admin.instructions.js?v=1.19.1-e48e1b0d"></script>
16
+ <script defer src="js/admin.logs.js?v=1.19.1-e48e1b0d"></script>
17
+ <script defer src="js/admin.maintenance.js?v=1.19.1-e48e1b0d"></script>
18
+ <script defer src="js/admin.config.js?v=1.19.1-e48e1b0d"></script>
19
+ <script defer src="js/admin.performance.js?v=1.19.1-e48e1b0d"></script>
20
+ <script defer src="js/admin.instances.js?v=1.19.1-e48e1b0d"></script>
21
+ <script defer src="js/admin.embeddings.js?v=1.19.1-e48e1b0d"></script>
22
+ <script defer src="js/admin.messaging.js?v=1.19.1-e48e1b0d"></script>
23
+ <script defer src="js/admin.sqlite.js?v=1.19.1-e48e1b0d"></script>
24
+ <script defer src="js/admin.boot.js?v=1.19.1-e48e1b0d"></script>
25
+ </head>
26
+ <body>
27
+ <div class="admin-container admin-root">
28
+ <div class="admin-header">
29
+ <h1>Index Server</h1>
30
+ <span id="storage-badge" class="storage-badge" style="display:none;margin-left:8px;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;letter-spacing:0.5px"></span>
31
+ <div id="instances-widget" class="instances-widget" style="display:none;">
32
+ <button id="instances-badge" class="instances-badge" title="Running instances">
33
+ <span id="instances-count">1</span> instance<span id="instances-plural">s</span>
34
+ </button>
35
+ <div id="instances-dropdown" class="instances-dropdown hidden">
36
+ <div class="instances-dropdown-header">Running Instances</div>
37
+ <div id="instances-list" class="instances-list"></div>
38
+ </div>
39
+ </div>
40
+ <div id="buildMeta" class="build-meta">Loading build metadata…</div>
41
+ <div class="admin-nav">
42
+ <!-- Added explicit data-section attributes so JS can reliably map buttons to sections after HTML refactor -->
43
+ <!-- Redundant inline onclick fallback keeps basic navigation working even if JS wiring changes -->
44
+ <button class="nav-btn active" data-section="overview" onclick="window.showSection && window.showSection('overview')">Overview</button>
45
+ <button class="nav-btn" data-section="config" onclick="window.showSection && window.showSection('config')">Configuration</button>
46
+ <button id="nav-sessions" class="nav-btn" data-section="sessions" onclick="window.showSection && window.showSection('sessions')">Sessions</button>
47
+ <button class="nav-btn" data-section="maintenance" onclick="window.showSection && window.showSection('maintenance')">Maintenance</button>
48
+ <button class="nav-btn" data-section="monitoring" onclick="window.showSection && window.showSection('monitoring')">Monitoring</button>
49
+ <button class="nav-btn" data-section="instructions" onclick="window.showSection && window.showSection('instructions')">Instructions</button>
50
+ <button class="nav-btn" data-section="graph" onclick="window.showSection && window.showSection('graph')">Graph</button>
51
+ <button class="nav-btn" data-section="embeddings" onclick="window.showSection && window.showSection('embeddings')">Embeddings</button>
52
+ <button class="nav-btn" data-section="messaging" onclick="window.showSection && window.showSection('messaging')">Messaging</button>
53
+ <button class="nav-btn" data-section="sqlite" id="nav-sqlite" onclick="window.showSection && window.showSection('sqlite')" style="display:none">SQLite</button>
54
+ </div>
55
+ </div>
56
+
57
+ <!-- Overview Section -->
58
+ <div id="overview-section" class="admin-section">
59
+ <div class="admin-grid">
60
+ <div class="admin-card">
61
+ <div class="card-header">
62
+ <div class="card-icon">📊</div>
63
+ <div class="card-title">System Statistics <a class="doc-link" href="/api/docs/overview" target="_blank" title="Panel documentation">?</a></div>
64
+ </div>
65
+ <div id="system-stats" class="loading metrics-list">Loading system statistics...</div>
66
+ </div>
67
+
68
+ <div class="admin-card">
69
+ <div class="card-header">
70
+ <div class="card-icon">💚</div>
71
+ <div class="card-title">System Health <a class="doc-link" href="/api/docs/overview" target="_blank" title="Panel documentation">?</a></div>
72
+ </div>
73
+ <div id="system-health" class="loading metrics-list">Loading system health...</div>
74
+ </div>
75
+
76
+ <div class="admin-card">
77
+ <div class="card-header">
78
+ <div class="card-icon">⚡</div>
79
+ <div class="card-title">Performance <a class="doc-link" href="/api/docs/overview" target="_blank" title="Panel documentation">?</a></div>
80
+ </div>
81
+ <div id="performance-stats" class="loading metrics-list">Loading performance data...</div>
82
+ </div>
83
+
84
+ <!-- index Status merged into System Statistics card -->
85
+ </div>
86
+
87
+ <!-- Individual Tool Metrics -->
88
+ <div class="admin-card mt-30">
89
+ <div class="card-header">
90
+ <div class="card-icon">🔧</div>
91
+ <div class="card-title">Individual Tool Call Metrics <a class="doc-link" href="/api/docs/overview" target="_blank" title="Panel documentation">?</a></div>
92
+ </div>
93
+ <div id="tool-metrics" class="loading">Loading tool metrics...</div>
94
+ </div>
95
+
96
+ <!-- Usage Signals Summary -->
97
+ <div class="admin-card mt-30">
98
+ <div class="card-header">
99
+ <div class="card-icon">📡</div>
100
+ <div class="card-title">Usage Signals <a class="doc-link" href="/api/docs/overview" target="_blank" title="Panel documentation">?</a></div>
101
+ </div>
102
+ <div id="usage-signals-panel" class="loading">Loading usage signals...</div>
103
+ </div>
104
+ </div>
105
+ <!-- Graph Section -->
106
+ <div id="graph-section" class="admin-section hidden">
107
+ <div class="admin-card">
108
+ <div class="card-header">
109
+ <div class="card-icon">🗺️</div>
110
+ <div class="card-title">Instruction Relationship Graph <a class="doc-link" href="/api/docs/graph" target="_blank" title="Panel documentation">?</a></div>
111
+ </div>
112
+ <div id="graph-toolbar" class="graph-toolbar toolbar mb-lg">
113
+ <label class="block order-0">Sel Categories
114
+ <select id="drill-categories" multiple size="5" onchange="coordinatedFilterChanged()"></select>
115
+ </label>
116
+ <label class="block order-1">Sel Instructions
117
+ <select id="drill-instructions" multiple size="5" onchange="coordinatedFilterChanged()"></select>
118
+ </label>
119
+ <div class="stack order-2">
120
+ <button class="action-btn sm" title="Reload categories list" onclick="refreshDrillCategories()">🔄 Load Categories</button>
121
+ <button class="action-btn sm btn-info" title="Load instructions (all or filtered by selected categories)" onclick="loadDrillInstructions()">📥 Load Instructions</button>
122
+ <button class="action-btn sm btn-success-gradient" title="Clear selections" onclick="clearSelections()">🧹 Clear Selections</button>
123
+ </div>
124
+ <label class="block layout-label" title="Graph layout engine (elk = hierarchical; default = Mermaid native)"><span class="lbl-icon">🧩</span><span>Layout</span>
125
+ <select id="graph-layout" class="form-input pill">
126
+ <option value="elk" selected>elk</option>
127
+ <option value="default">default</option>
128
+ </select>
129
+ </label>
130
+ <div class="actions order-6">
131
+ <button id="graph-refresh-btn" class="action-btn btn-success-gradient" title="Refresh Mermaid diagram from current filters">🔄 Refresh Graph</button>
132
+ </div>
133
+ </div>
134
+
135
+ <div id="graph-meta" class="graph-meta"></div>
136
+ <div id="graph-meta2" class="graph-meta2"></div>
137
+
138
+ <!-- Advanced Options (collapsed) -->
139
+ <details id="graph-advanced-options" class="graph-details">
140
+ <summary>⚙️ Advanced Options</summary>
141
+ <div class="graph-advanced-content">
142
+ <div class="flags">
143
+ <label class="chk simple"><input id="graph-enrich" type="checkbox" checked> Enrich</label>
144
+ <label class="chk simple"><input id="graph-categories" type="checkbox" checked> Categories</label>
145
+ <label class="chk simple"><input id="graph-usage" type="checkbox"> Usage</label>
146
+ </div>
147
+ <div class="flags edge-layout-group">
148
+ <label class="block etypes-label" title="Filter specific edge types (comma separated). Leave blank for all."><span class="lbl-icon">🔗</span><span>Edge Types</span>
149
+ <input id="graph-edgeTypes" class="form-input pill" placeholder="all (comma list)" autocomplete="off">
150
+ </label>
151
+ </div>
152
+ <div class="stack">
153
+ <label class="chk" title="Toggle high Mermaid maxEdges secure limit (forces re-init)"><input id="mermaid-high-edges" type="checkbox"> High edge cap</label>
154
+ <label class="chk" title="Enable Large Graph Mode (raises maxEdges & maxTextSize caps)"><input id="mermaid-large-graph" type="checkbox"> Large graph mode</label>
155
+ <label class="chk simple" title="Enable debug logging"><input id="graph-debug" type="checkbox"> Debug</label>
156
+ </div>
157
+ </div>
158
+ </details>
159
+ </div>
160
+
161
+ <!-- DIAGRAM FIRST -->
162
+ <div class="admin-card mt-xl" id="graph-render-card">
163
+ <div class="graph-zoom-controls">
164
+ <button id="graph-zoom-in" class="action-btn sm" title="Zoom in">➕</button>
165
+ <button id="graph-zoom-out" class="action-btn sm" title="Zoom out">➖</button>
166
+ <button id="graph-zoom-reset" class="action-btn sm" title="Reset zoom">🔍</button>
167
+ <button id="graph-fullscreen-btn" class="action-btn sm" title="Toggle fullscreen">⛶</button>
168
+ </div>
169
+ <div class="card-header">
170
+ <span class="card-icon">🧪</span>
171
+ <span class="card-title">Rendered Diagram <a class="doc-link" href="/api/docs/graph" target="_blank" title="Panel documentation">?</a></span>
172
+ </div>
173
+ <div id="graph-mermaid-rendered" class="graph-rendered">
174
+ <div class="graph-loading-skeleton">Loading diagram...</div>
175
+ <div id="graph-mermaid-svg" class="graph-svg-min">(diagram not loaded)</div>
176
+ </div>
177
+ <div class="graph-render-note">Copy source into <a href="https://mermaid.live" target="_blank" rel="noopener" class="mermaid-link">Mermaid Live Editor</a> for advanced tweaks. Layout 'elk' leverages experimental ELK engine; fallback auto-applies legacy init directive if frontmatter unsupported.</div>
178
+ </div>
179
+
180
+ <!-- SOURCE SECOND (collapsed) -->
181
+ <details id="graph-source-details" class="graph-details mt-xl">
182
+ <summary>📝 Mermaid Source</summary>
183
+ <div class="admin-card">
184
+ <div class="source-actions">
185
+ <button id="graph-copy-btn" class="action-btn btn-info" title="Copy Mermaid source to clipboard" onclick="window.copyMermaidSource && window.copyMermaidSource()">📋 Copy Source</button>
186
+ <button id="graph-edit-btn" class="action-btn btn-edit" title="Edit Mermaid source inline" onclick="window.toggleGraphEdit && window.toggleGraphEdit()">✏️ Edit Source</button>
187
+ <button id="graph-apply-btn" class="action-btn btn-success-gradient" style="display:none" title="Apply manual edits" onclick="window.applyGraphEdit && window.applyGraphEdit()">💾 Apply Edits</button>
188
+ <button id="graph-cancel-btn" class="action-btn danger" style="display:none;" title="Cancel manual edits" onclick="window.cancelGraphEdit && window.cancelGraphEdit()">↩️ Cancel Edit</button>
189
+ <label class="graph-auto-label"><input type="checkbox" id="graph-auto-render" checked> auto-render</label>
190
+ </div>
191
+ <div id="graph-mermaid-wrapper" class="graph-wrapper">
192
+ <pre id="graph-mermaid" class="graph-source-pre">(loading graph...)</pre>
193
+ </div>
194
+ <div class="graph-source-note">Mermaid source shown above. Rendered diagram above uses same code.</div>
195
+ </div>
196
+ </details>
197
+
198
+ </div>
199
+
200
+ <!-- Configuration Section -->
201
+ <div id="config-section" class="admin-section hidden">
202
+ <div class="admin-card">
203
+ <div class="card-header">
204
+ <div class="card-icon">⚙️</div>
205
+ <div class="card-title">Server Configuration <a class="doc-link" href="/api/docs/config" target="_blank" title="Panel documentation">?</a></div>
206
+ </div>
207
+ <div id="config-form" class="loading config-loading">Loading configuration...</div>
208
+ </div>
209
+ </div>
210
+
211
+ <!-- Sessions Section -->
212
+ <div id="sessions-section" class="admin-section hidden">
213
+ <div class="admin-grid grid-single-col">
214
+ <div class="admin-card">
215
+ <div class="card-header">
216
+ <div class="card-icon">👥</div>
217
+ <div class="card-title">Active Admin Sessions <a class="doc-link" href="/api/docs/sessions" target="_blank" title="Panel documentation">?</a></div>
218
+ </div>
219
+ <div id="sessions-list" class="loading index-list">Loading sessions...</div>
220
+ <div id="sessions-pagination" class="index-pagination sessions-pagination" style="display:none">
221
+ <button data-role="prev-page" class="action-btn page-btn" onclick="setSessionsPage(window.__sessionsPage-1)">Prev</button>
222
+ <div data-role="page-info" class="page-info">Page 1 / 1</div>
223
+ <button data-role="next-page" class="action-btn page-btn" onclick="setSessionsPage(window.__sessionsPage+1)">Next</button>
224
+ <label class="page-size-label">
225
+ Size
226
+ <select data-role="page-size" class="form-input page-size-select" onchange="changeSessionsPageSize(parseInt(this.value,10))">
227
+ <option value="10">10</option>
228
+ <option value="25" selected>25</option>
229
+ <option value="50">50</option>
230
+ <option value="100">100</option>
231
+ </select>
232
+ </label>
233
+ </div>
234
+ <div class="session-actions">
235
+ <button class="action-btn">Create Test Session</button>
236
+ <button class="action-btn">🔄 Refresh</button>
237
+ </div>
238
+ </div>
239
+ <div class="admin-card mt-xxl">
240
+ <div class="card-header">
241
+ <div class="card-icon">🔌</div>
242
+ <div class="card-title">Active WebSocket Connections <a class="doc-link" href="/api/docs/sessions" target="_blank" title="Panel documentation">?</a></div>
243
+ </div>
244
+ <div id="connections-list" class="loading">Loading connections...</div>
245
+ </div>
246
+ <div class="admin-card mt-xxl">
247
+ <div class="card-header">
248
+ <div class="card-icon">🗂️</div>
249
+ <div class="card-title">Session History <a class="doc-link" href="/api/docs/sessions" target="_blank" title="Panel documentation">?</a></div>
250
+ </div>
251
+ <div class="flex-row mb-md">
252
+ <label class="form-label-sm">Limit
253
+ <select id="session-history-limit" class="form-input w-80" onchange="refreshSessionHistory()">
254
+ <option value="25">25</option>
255
+ <option value="50" selected>50</option>
256
+ <option value="100">100</option>
257
+ <option value="250">250</option>
258
+ </select>
259
+ </label>
260
+ <button class="action-btn">🔄 Refresh History</button>
261
+ </div>
262
+ <div id="session-history-list" class="loading index-list">History not loaded...</div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+
267
+ <!-- Maintenance Section -->
268
+ <div id="maintenance-section" class="admin-section hidden">
269
+ <div class="admin-grid">
270
+ <div class="admin-card">
271
+ <div class="card-header">
272
+ <div class="card-icon">🔧</div>
273
+ <div class="card-title">Maintenance Control <a class="doc-link" href="/api/docs/maintenance" target="_blank" title="Panel documentation">?</a></div>
274
+ </div>
275
+ <div id="maintenance-control" class="loading">Loading maintenance status...</div>
276
+ </div>
277
+
278
+ <div class="admin-card">
279
+ <div class="card-header">
280
+ <div class="card-icon">💾</div>
281
+ <div class="card-title">System Operations <a class="doc-link" href="/api/docs/maintenance" target="_blank" title="Panel documentation">?</a></div>
282
+ </div>
283
+ <div class="action-buttons sys-ops flex-row">
284
+ <button id="btn-create-backup" class="action-btn primary" data-op="create-backup" title="Create a new instruction + data backup">💾 Create Backup</button>
285
+ <button id="btn-clear-caches" class="action-btn warning" data-op="clear-caches" title="Clear in-memory caches (manifest, stats, etc.)">🗑️ Clear Caches</button>
286
+ <button id="btn-restart-server" class="action-btn danger" data-op="restart-server" title="Gracefully restart the server">🔄 Restart Server</button>
287
+ </div>
288
+ <div class="action-buttons sys-ops flex-row" style="margin-top:8px;">
289
+ <button class="action-btn success" onclick="exportBackupToFile()" title="Export the selected backup to a downloadable JSON file">📥 Backup to File</button>
290
+ <button class="action-btn primary" onclick="importBackupFromFile()" title="Import a backup from a JSON file">📤 Restore from File</button>
291
+ <input type="file" id="backup-file-input" accept=".json" style="display:none" onchange="handleBackupFileSelected(event)" />
292
+ </div>
293
+ <div class="mt-lg">
294
+ <div class="restore-header">Restore Backup <button class="action-btn page-btn font-sm">Refresh</button></div>
295
+ <div id="backup-restore-area" class="restore-area">
296
+ <select id="backup-select" class="form-input backup-select">
297
+ <option value="">(no backups)</option>
298
+ </select>
299
+ <button id="btn-restore-backup" class="action-btn success" data-op="restore-backup" title="Restore selected backup">♻️ Restore</button>
300
+ <span id="backup-restore-status" class="backup-status"></span>
301
+ </div>
302
+ <div id="backup-list-meta" class="backup-meta"></div>
303
+ </div>
304
+ </div>
305
+
306
+ <!-- Signal Groom Card -->
307
+ <div class="admin-card">
308
+ <div class="card-header">
309
+ <div class="card-icon">📡</div>
310
+ <div class="card-title">Signal Feedback Groom</div>
311
+ </div>
312
+ <div style="padding:8px 0;font-size:12px;opacity:.7;">
313
+ Apply usage signals (helpful/applied/not-relevant/outdated) to instruction priority and requirement fields.
314
+ </div>
315
+ <div class="action-buttons sys-ops flex-row">
316
+ <button class="action-btn primary" onclick="runSignalGroom(true)">🔍 Dry Run</button>
317
+ <button class="action-btn warning" onclick="if(confirm('Apply signal feedback to instructions?')) runSignalGroom(false)">⚡ Apply Signals</button>
318
+ </div>
319
+ <div id="signal-groom-status" class="mt-sm" style="font-size:12px;"></div>
320
+ </div>
321
+ </div>
322
+ </div>
323
+
324
+ <!-- Monitoring Section -->
325
+ <div id="monitoring-section" class="admin-section hidden">
326
+ <div class="admin-card">
327
+ <div class="card-header">
328
+ <div class="card-icon"><svg viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 14 10 14 12 10 16 20 18 14 22 14"/><circle cx="6" cy="6" r="2"/></svg></div>
329
+ <div class="card-title">Real-time Monitoring <a class="doc-link" href="/api/docs/monitoring" target="_blank" title="Panel documentation">?</a></div>
330
+ </div>
331
+ <div id="monitoring-data" class="loading monitoring-loading">Loading monitoring data...</div>
332
+ </div>
333
+ <div class="admin-card mt-xxl">
334
+ <div class="card-header">
335
+ <div class="card-icon">🧪</div>
336
+ <div class="card-title">Synthetic Activity <a class="doc-link" href="/api/docs/monitoring" target="_blank" title="Panel documentation">?</a></div>
337
+ </div>
338
+ <div class="synthetic-form">
339
+ <div>
340
+ <label class="form-label form-label-sm">Iterations</label>
341
+ <input id="synthetic-iterations" class="form-input form-input-sm" type="number" value="25" />
342
+ </div>
343
+ <div>
344
+ <label class="form-label form-label-sm">Concurrency</label>
345
+ <input id="synthetic-concurrency" class="form-input form-input-sm" type="number" value="3" />
346
+ </div>
347
+ <button id="synthetic-run-btn" class="action-btn">Run Synthetic Activity</button>
348
+ <div id="synthetic-output" class="synthetic-output"></div>
349
+ </div>
350
+ <div class="synthetic-note">Executes random safe tools to exercise metrics &amp; health. <span id="synthetic-last-meta" class="text-italic"></span></div>
351
+ <div id="synthetic-traces-wrapper" class="mt-lg" style="display:none">
352
+ <div class="trace-header">
353
+ Per‑Call Trace
354
+ <label class="trace-toggle-label">
355
+ <input id="synthetic-trace-toggle" type="checkbox" checked onchange="toggleSyntheticTraceVisibility()" /> show
356
+ </label>
357
+ </div>
358
+ <div class="trace-scroll">
359
+ <table class="trace-table">
360
+ <thead class="trace-thead">
361
+ <tr>
362
+ <th class="trace-th">#</th>
363
+ <th class="trace-th">Tool</th>
364
+ <th class="trace-th">Success</th>
365
+ <th class="trace-th">Duration</th>
366
+ <th class="trace-th">Error</th>
367
+ </tr>
368
+ </thead>
369
+ <tbody id="synthetic-traces-body"></tbody>
370
+ </table>
371
+ </div>
372
+ </div>
373
+ </div>
374
+
375
+ <!-- Log Viewer Section -->
376
+ <div class="admin-card mt-xxl">
377
+ <div class="card-header">
378
+ <div class="card-icon">📋</div>
379
+ <div class="card-title">Server Logs <a class="doc-link" href="/api/docs/monitoring" target="_blank" title="Panel documentation">?</a></div>
380
+ </div>
381
+ <div class="log-controls">
382
+ <div>
383
+ <label class="form-label text-xs">Lines</label>
384
+ <input id="log-lines" class="form-input w-80" type="number" value="100" />
385
+ </div>
386
+ <button id="log-refresh-btn" class="action-btn" onclick="window.loadLogs && window.loadLogs()">🔄 Refresh</button>
387
+ <button id="log-tail-btn" class="action-btn" onclick="window.toggleLogTail && window.toggleLogTail()">▶️ Start Tail</button>
388
+ <button id="log-clear-btn" class="action-btn" onclick="window.clearLogViewer && window.clearLogViewer()">🗑️ Clear</button>
389
+ <div id="log-status" class="log-status-text"></div>
390
+ </div>
391
+ <!-- Log content container id expected by scripts (was log-viewer) -->
392
+ <div id="log-content" class="log-content-box">
393
+ <div class="log-placeholder">Click "Refresh" to load server logs...</div>
394
+ </div>
395
+ <div class="log-note">
396
+ Real-time server log viewer. Set INDEX_SERVER_LOG_FILE to a path OR simply '1' (auto => logs/mcp-server.log).
397
+ </div>
398
+ </div>
399
+ </div>
400
+
401
+ <!-- Embeddings Visualization Section -->
402
+ <div id="embeddings-section" class="admin-section hidden">
403
+ <div class="admin-card" style="padding:0;overflow:hidden">
404
+ <div class="emb-layout">
405
+ <!-- Sidebar -->
406
+ <div class="emb-sidebar">
407
+ <div class="emb-section">
408
+ <input id="emb-search" class="emb-search-box" type="text" placeholder="Search instruction IDs…" />
409
+ <div class="emb-controls">
410
+ <button class="emb-btn" data-emb-action="compute">Compute</button>
411
+ <button class="emb-btn" data-emb-action="load">Load</button>
412
+ <button class="emb-btn" data-emb-action="reset">Reset</button>
413
+ <button class="emb-btn" id="emb-norm-btn" data-emb-action="norm">Norm</button>
414
+ </div>
415
+ <span id="emb-status" class="emb-status-text"></span>
416
+ </div>
417
+ <div class="emb-section" id="emb-stats-section">
418
+ <div class="emb-section-title">Statistics</div>
419
+ <div id="emb-stats"></div>
420
+ </div>
421
+ <div class="emb-section">
422
+ <div class="emb-section-title">Categories</div>
423
+ <div id="emb-legend"></div>
424
+ </div>
425
+ <div class="emb-section">
426
+ <div class="emb-section-title">Most Similar Pairs</div>
427
+ <div id="emb-similar"></div>
428
+ </div>
429
+ <div class="emb-section">
430
+ <div class="emb-section-title">Selected Point</div>
431
+ <div id="emb-detail"><em style="color:var(--admin-text-dim)">Click a point to inspect</em></div>
432
+ </div>
433
+ </div>
434
+ <!-- Canvas area -->
435
+ <div class="emb-canvas-wrap">
436
+ <canvas id="embeddings-canvas"></canvas>
437
+ <div id="emb-tooltip" class="emb-tooltip"></div>
438
+ </div>
439
+ </div>
440
+ </div>
441
+ </div>
442
+
443
+ <!-- Instruction Management Section -->
444
+ <div id="instructions-section" class="admin-section hidden">
445
+ <div class="admin-card">
446
+ <div class="card-header">
447
+ <div class="card-icon">📚</div>
448
+ <div class="card-title">Instruction index <a class="doc-link" href="/api/docs/instructions" target="_blank" title="Panel documentation">?</a></div>
449
+ </div>
450
+ <div class="instr-toolbar">
451
+ <button class="action-btn" onclick="loadInstructions()">🔄 Refresh</button>
452
+ <button class="action-btn" onclick="showCreateInstruction()">➕ Create</button>
453
+ <input id="instruction-filter" placeholder="Filter by name (supports regex)..." class="form-input instr-filter" oninput="filterInstructions()" />
454
+ <label class="regex-toggle" title="Enable regex matching"><input type="checkbox" id="instruction-regex-toggle" onchange="filterInstructions()" /><span class="regex-label">.*</span></label>
455
+ <select id="instruction-category-filter" class="form-input w-130" onchange="filterInstructions()">
456
+ <option value="">All Categories</option>
457
+ <!-- Categories will be populated dynamically -->
458
+ </select>
459
+ <select id="instruction-size-filter" class="form-input w-120" onchange="filterInstructions()">
460
+ <option value="">All Sizes</option>
461
+ <option value="small">small</option>
462
+ <option value="medium">medium</option>
463
+ <option value="large">large</option>
464
+ </select>
465
+ <select id="instruction-sort" class="form-input w-150" onchange="filterInstructions()">
466
+ <option value="name-asc">Name A-Z</option>
467
+ <option value="name-desc">Name Z-A</option>
468
+ <option value="size-asc">Size ↑</option>
469
+ <option value="size-desc">Size ↓</option>
470
+ <option value="mtime-desc">Modified ↓</option>
471
+ <option value="mtime-asc">Modified ↑</option>
472
+ <option value="category">Category</option>
473
+ <option value="usage-desc">Usage ↓</option>
474
+ <option value="signal">Signal</option>
475
+ </select>
476
+ </div>
477
+ <!-- Global Search Row -->
478
+ <div class="instr-search-row">
479
+ <input id="instruction-global-search" placeholder="Global search (id, title, body, categories — supports regex)..." class="form-input instr-global" />
480
+ <label class="regex-toggle" title="Enable regex matching for global search"><input type="checkbox" id="instruction-global-regex-toggle" /><span class="regex-label">.*</span></label>
481
+ <button id="instruction-global-search-btn" class="action-btn btn-purple" onclick="(function(){ try { if(window.performGlobalInstructionSearch){ const v = document.getElementById('instruction-global-search')?.value||''; window.performGlobalInstructionSearch(v); } } catch(e){ console.warn('inline global search fallback failed', e); } })();">🌍 Search All</button>
482
+ <span class="text-dim">Fallback substring search across all instruction files.</span>
483
+ </div>
484
+ <div id="instruction-global-results" class="instr-results"></div>
485
+ <div id="instructions-list" class="loading">Loading instructions...</div>
486
+ <div id="instruction-pagination" class="mt-md"></div>
487
+ <!-- Editor will be dynamically repositioned above the list when activated -->
488
+ <div id="instruction-editor" class="hidden mt-lg">
489
+ <h3 id="instruction-editor-title" class="mb-sm">New Instruction</h3>
490
+ <div class="form-group">
491
+ <label class="form-label">File Name (no extension)</label>
492
+ <input id="instruction-filename" class="form-input" placeholder="example-instruction" />
493
+ </div>
494
+ <div class="form-group mt-sm">
495
+ <label class="form-label">JSON Content</label>
496
+ <textarea id="instruction-content" class="form-input instr-textarea" oninput="updateInstructionEditorDiagnostics()" placeholder="{\n \"id\": \"example\",\n \"title\": \"Example Instruction\"\n}"></textarea>
497
+ <div id="instruction-diagnostics" class="instr-diagnostics">
498
+ <em>Editor idle.</em>
499
+ </div>
500
+ <div id="instruction-preview-container" class="hidden instr-preview-wrap">
501
+ <div class="instr-preview-header">
502
+ <span class="instr-preview-label">Body Preview (Markdown)</span>
503
+ </div>
504
+ <div id="instruction-preview" class="instr-preview-content"></div>
505
+ </div>
506
+ <div id="instruction-diff-container" class="hidden instr-diff-wrap">
507
+ <div class="instr-diff-label">Diff (original vs current)</div>
508
+ <pre id="instruction-diff" class="instr-diff-pre"></pre>
509
+ </div>
510
+ </div>
511
+ <div class="instr-actions">
512
+ <button id="instruction-save-btn" class="action-btn" onclick="saveInstruction()">💾 Save</button>
513
+ <button id="instruction-preview-btn" class="action-btn btn-green" onclick="toggleInstructionPreview()">📖 Preview</button>
514
+ <button id="instruction-format-btn" class="action-btn btn-info" onclick="formatInstructionJson()">🧹 Format</button>
515
+ <button id="instruction-diff-btn" class="action-btn btn-purple" onclick="toggleInstructionDiff()">🔍 Diff</button>
516
+ <button id="instruction-template-btn" class="action-btn btn-teal" onclick="applyInstructionTemplate()">📐 Template</button>
517
+ <button id="instruction-cancel-btn" class="action-btn warning" onclick="cancelEditInstruction()">✖ Cancel</button>
518
+ </div>
519
+ </div>
520
+ </div>
521
+ </div>
522
+
523
+ <!-- Messaging Section -->
524
+ <div id="messaging-section" class="admin-section hidden">
525
+ <div class="admin-card">
526
+ <div class="card-header">
527
+ <div class="card-icon">💬</div>
528
+ <div class="card-title">Inter-Agent Messaging</div>
529
+ </div>
530
+ <div class="msg-layout">
531
+ <!-- Sidebar -->
532
+ <div class="msg-sidebar">
533
+ <div style="padding:0 12px 8px;font-size:14px;font-weight:600;color:var(--admin-text)">Messages <span id="msg-summary" style="font-weight:400;font-size:12px;color:var(--admin-text-dim)"></span></div>
534
+ <div class="msg-sidebar-heading">CHANNELS</div>
535
+ <div id="messaging-channel-list"></div>
536
+ <div class="msg-sidebar-heading" style="margin-top:12px">SENDERS</div>
537
+ <div id="messaging-sender-list"></div>
538
+ </div>
539
+ <!-- Main content -->
540
+ <div class="msg-main">
541
+ <!-- Toolbar -->
542
+ <div class="msg-toolbar">
543
+ <input id="msg-search" type="text" placeholder="Search messages…" class="form-input"
544
+ data-action="filter-input" />
545
+ <select id="msg-priority-filter" data-action="filter-priority">
546
+ <option value="">Priority</option>
547
+ <option value="critical">Critical</option>
548
+ <option value="high">High</option>
549
+ <option value="normal">Normal</option>
550
+ <option value="low">Low</option>
551
+ </select>
552
+ <select id="msg-sort" data-action="sort">
553
+ <option value="newest">Newest</option>
554
+ <option value="oldest">Oldest</option>
555
+ <option value="priority">Priority</option>
556
+ </select>
557
+ <div class="msg-toolbar-group">
558
+ <button class="msg-toggle-btn active" id="msg-view-list" data-action="view-mode" data-mode="list">List</button>
559
+ <button class="msg-toggle-btn" id="msg-view-timeline" data-action="view-mode" data-mode="timeline">Timeline</button>
560
+ </div>
561
+ <label style="display:flex;align-items:center;gap:4px;font-size:12px;color:var(--admin-text-dim);cursor:pointer;white-space:nowrap">
562
+ <input type="checkbox" id="msg-select-all" data-action="select-all" style="accent-color:var(--admin-accent)"> Select All
563
+ </label>
564
+ <span class="msg-toolbar-count" id="msg-count"></span>
565
+ <button class="action-btn" data-action="refresh">🔄 Refresh</button>
566
+ <button class="action-btn" data-action="download">⬇ Download</button>
567
+ </div>
568
+ <!-- Message list -->
569
+ <div id="messaging-message-list"></div>
570
+ <div id="messaging-pagination" class="msg-pagination"></div>
571
+ </div>
572
+ </div>
573
+ <!-- Compose -->
574
+ <div class="msg-compose">
575
+ <h4>Compose Message</h4>
576
+ <div class="msg-compose-row">
577
+ <input id="msg-compose-channel" type="text" placeholder="Channel" class="form-input" />
578
+ <input id="msg-compose-sender" type="text" placeholder="Sender (default: dashboard)" class="form-input" />
579
+ <input id="msg-compose-recipients" type="text" placeholder="Recipients (* = broadcast)" class="form-input" value="*" />
580
+ </div>
581
+ <div class="msg-compose-row">
582
+ <input id="msg-compose-tags" type="text" placeholder="Tags (comma-separated)" class="form-input" />
583
+ <select id="msg-compose-priority" class="form-input" style="max-width:140px">
584
+ <option value="normal">Normal</option>
585
+ <option value="low">Low</option>
586
+ <option value="high">High</option>
587
+ <option value="critical">Critical</option>
588
+ </select>
589
+ </div>
590
+ <textarea id="msg-compose-body" placeholder="Message body…" class="form-input" rows="3" style="width:100%;resize:vertical"></textarea>
591
+ <button class="action-btn" style="margin-top:8px" data-action="send">📤 Send</button>
592
+ </div>
593
+ </div>
594
+ <div id="messaging-detail"></div>
595
+ </div>
596
+
597
+ <!-- SQLite Section -->
598
+ <div id="sqlite-section" class="admin-section hidden">
599
+ <div class="admin-grid">
600
+ <div class="admin-card">
601
+ <div class="card-header">
602
+ <div class="card-icon">🗄️</div>
603
+ <div class="card-title">Database Info</div>
604
+ </div>
605
+ <div id="sqlite-info" class="loading metrics-list">Loading database info…</div>
606
+ </div>
607
+ <div class="admin-card">
608
+ <div class="card-header">
609
+ <div class="card-icon">📊</div>
610
+ <div class="card-title">Table Statistics</div>
611
+ </div>
612
+ <div id="sqlite-tables" class="metrics-list">—</div>
613
+ </div>
614
+ <div class="admin-card">
615
+ <div class="card-header">
616
+ <div class="card-icon">🔧</div>
617
+ <div class="card-title">Maintenance</div>
618
+ </div>
619
+ <div class="sqlite-maintenance-btns" style="display:flex;gap:8px;flex-wrap:wrap;padding:8px 0">
620
+ <button class="action-btn" data-sqlite-action="vacuum">🧹 VACUUM</button>
621
+ <button class="action-btn" data-sqlite-action="optimize">⚡ Optimize FTS5</button>
622
+ <button class="action-btn" data-sqlite-action="integrity">✅ Integrity Check</button>
623
+ <button class="action-btn" data-sqlite-action="analyze">📈 ANALYZE</button>
624
+ <button class="action-btn" data-sqlite-action="reindex">🔄 REINDEX</button>
625
+ <button class="action-btn" data-sqlite-action="groom">🧽 Groom</button>
626
+ </div>
627
+ <div id="sqlite-maintenance-result" style="margin-top:8px;font-size:13px"></div>
628
+ </div>
629
+ </div>
630
+ <div class="admin-grid" style="margin-top:16px">
631
+ <div class="admin-card">
632
+ <div class="card-header">
633
+ <div class="card-icon">💾</div>
634
+ <div class="card-title">Backup &amp; Restore</div>
635
+ </div>
636
+ <div style="display:flex;gap:8px;flex-wrap:wrap;padding:8px 0">
637
+ <button class="action-btn" data-sqlite-action="backup">📦 Create Backup</button>
638
+ <button class="action-btn" data-sqlite-action="wal-checkpoint">📝 WAL Checkpoint</button>
639
+ </div>
640
+ <div id="sqlite-backup-result" style="margin-top:8px;font-size:13px"></div>
641
+ <div style="margin-top:12px">
642
+ <div style="font-weight:600;font-size:13px;color:var(--admin-text,#e2e8f0);margin-bottom:6px">Available Backups</div>
643
+ <div id="sqlite-backups-list" style="font-size:13px;color:var(--admin-text-dim,#94a3b8)">Loading…</div>
644
+ </div>
645
+ </div>
646
+ <div class="admin-card">
647
+ <div class="card-header">
648
+ <div class="card-icon">🔄</div>
649
+ <div class="card-title">Migration &amp; Recovery</div>
650
+ </div>
651
+ <div style="display:flex;gap:8px;flex-wrap:wrap;padding:8px 0">
652
+ <button class="action-btn" data-sqlite-action="migrate">📥 Import from JSON</button>
653
+ <button class="action-btn" data-sqlite-action="export">📤 Export to JSON</button>
654
+ <button class="action-btn" data-sqlite-action="reset" style="background:#7f1d1d;border-color:#ef4444">🗑️ Reset Database</button>
655
+ </div>
656
+ <div id="sqlite-migration-result" style="margin-top:8px;font-size:13px"></div>
657
+ </div>
658
+ </div>
659
+ <div class="admin-card" style="margin-top:16px">
660
+ <div class="card-header">
661
+ <div class="card-icon">🔍</div>
662
+ <div class="card-title">Query Console</div>
663
+ </div>
664
+ <div style="padding:8px 0">
665
+ <textarea id="sqlite-query-input" class="form-input" rows="4" style="width:100%;font-family:monospace;font-size:13px;resize:vertical" placeholder="SELECT * FROM instructions LIMIT 10"></textarea>
666
+ <div style="display:flex;gap:8px;margin-top:8px;align-items:center">
667
+ <button class="action-btn" data-sqlite-action="run-query">▶ Run Query</button>
668
+ <span id="sqlite-query-status" style="font-size:12px;color:var(--admin-text-dim)"></span>
669
+ </div>
670
+ </div>
671
+ <div id="sqlite-query-result" style="margin-top:8px;overflow-x:auto"></div>
672
+ </div>
673
+ </div>
674
+ </div>
675
+
676
+ <script>
677
+ // Admin Panel JavaScript
678
+ let currentSection = 'overview';
679
+ let refreshInterval;
680
+ // Track if /api/admin/stats responded successfully on most recent loadOverviewData()
681
+ // Used to downgrade health display (memory/errors) when stats are missing.
682
+ // NOTE: use window-scoped flag so extracted overview script and inline health renderer share state
683
+ // Previously a local 'let statsAvailable' caused displaySystemHealth() to always treat statistics as unavailable
684
+ // which produced a persistent 'Statistics unavailable' issue despite stats loading successfully.
685
+ window.statsAvailable = false;
686
+ // WebSocket for live events (metrics + synthetic trace streaming)
687
+ let dashboardSocket = null;
688
+ let lastSyntheticRunId = null;
689
+ function initDashboardSocket(){
690
+ try {
691
+ if (dashboardSocket && dashboardSocket.readyState === WebSocket.OPEN) return;
692
+ const proto = (location.protocol === 'https:') ? 'wss' : 'ws';
693
+ dashboardSocket = new WebSocket(`${proto}://${location.host}/ws`);
694
+ dashboardSocket.onopen = ()=>{/* noop */};
695
+ dashboardSocket.onmessage = ev => {
696
+ try {
697
+ const msg = JSON.parse(ev.data);
698
+ if (msg.type === 'synthetic_trace' && msg.data) {
699
+ handleSyntheticTrace(msg.data);
700
+ }
701
+ } catch {/* ignore */}
702
+ };
703
+ dashboardSocket.onclose = ()=>{ setTimeout(initDashboardSocket, 4000); };
704
+ } catch(e){ console.warn('ws init failed', e); }
705
+ }
706
+ function handleSyntheticTrace(data){
707
+ if (!data || !data.runId) return;
708
+ // If new run starts while traces visible, auto-clear
709
+ if (lastSyntheticRunId && data.runId !== lastSyntheticRunId) {
710
+ const body = document.getElementById('synthetic-traces-body');
711
+ if (body) body.innerHTML='';
712
+ }
713
+ lastSyntheticRunId = data.runId;
714
+ const body = document.getElementById('synthetic-traces-body');
715
+ if (!body) return;
716
+ const clr = data.success ? '#0a0' : '#a00';
717
+ const err = data.error ? String(data.error).slice(0,80) : '';
718
+ const skipped = data.skipped ? ' (skipped)' : '';
719
+ const tr = document.createElement('tr');
720
+ tr.innerHTML = `<td class="trace-cell">${data.seq}</td>
721
+ <td class="trace-cell trace-mono">${data.method}${skipped}</td>
722
+ <td class="trace-cell ${data.success ? 'text-ok' : 'text-fail'}">${data.success?'✓':'✗'}</td>
723
+ <td class="trace-cell">${data.durationMs}ms</td>
724
+ <td class="trace-cell ${data.error ? 'text-err' : 'text-muted'}">${err}</td>`;
725
+ body.appendChild(tr);
726
+ const wrap = document.getElementById('synthetic-traces-wrapper');
727
+ if (wrap && wrap.style.display === 'none') wrap.style.display='block';
728
+ }
729
+
730
+ // Initialize admin panel
731
+ document.addEventListener('DOMContentLoaded', function() {
732
+ showSection('overview');
733
+ startAutoRefresh();
734
+ // Attempt to auto-create a dashboard admin session if none exists for this browser tab
735
+ try { maybeEnsureAdminSession(); } catch(e) { console.warn('auto session create failed', e); }
736
+ initDashboardSocket();
737
+ });
738
+
739
+ function showSection(section) {
740
+ // Hide all sections
741
+ document.querySelectorAll('.admin-section').forEach(s => s.classList.add('hidden'));
742
+
743
+ // Show selected section if present
744
+ const activeSection = document.getElementById(section + '-section');
745
+ if (activeSection) activeSection.classList.remove('hidden');
746
+
747
+ // Update nav buttons without relying on implicit event
748
+ document.querySelectorAll('.nav-btn').forEach(btn => {
749
+ const isTarget = btn.getAttribute('onclick')?.includes(`showSection('${section}')`);
750
+ if (isTarget) {
751
+ btn.classList.add('active');
752
+ } else {
753
+ btn.classList.remove('active');
754
+ }
755
+ });
756
+
757
+ currentSection = section;
758
+ loadSectionData(section);
759
+ }
760
+
761
+ function loadSectionData(section) {
762
+ switch(section) {
763
+ case 'overview':
764
+ loadOverviewData();
765
+ break;
766
+ case 'graph':
767
+ initGraphScopeDefaults();
768
+ break;
769
+ case 'config':
770
+ loadConfiguration();
771
+ break;
772
+ case 'sessions':
773
+ loadSessions();
774
+ break;
775
+ case 'maintenance':
776
+ loadMaintenanceStatus();
777
+ loadBackups();
778
+ break;
779
+ case 'monitoring':
780
+ loadMonitoringData();
781
+ ensureMonitoringPoll();
782
+ break;
783
+ case 'instructions':
784
+ loadInstructions();
785
+ break;
786
+ case 'messaging':
787
+ if (window.initMessaging) window.initMessaging();
788
+ break;
789
+ }
790
+ }
791
+
792
+ // Graph logic was extracted to js/admin.graph.js?v=1.19.1-e48e1b0d
793
+ // Functions available globally: reloadGraphMermaid, initGraphScopeDefaults, copyMermaidSource, toggleGraphEdit, applyGraphEdit, cancelGraphEdit, refreshDrillCategories, loadDrillInstructions, clearSelections
794
+
795
+ <!-- overview functions moved to js/admin.overview.js?v=1.19.1-e48e1b0d -->
796
+
797
+ // Lightweight overview-level maintenance display (optional)
798
+ // Intentionally minimal to avoid blocking overview rendering.
799
+ // If an element with id 'maintenance-overview' exists, populate it; otherwise no-op.
800
+ function displayMaintenanceInfo(maintenance) {
801
+ try {
802
+ const el = document.getElementById('maintenance-overview');
803
+ if (!el || !maintenance) return; // Safe no-op if overview element not present
804
+ const mode = maintenance.maintenanceMode ? 'ENABLED' : 'Disabled';
805
+ el.innerHTML = `
806
+ <div class="stat-row">
807
+ <span class="stat-label">Last Backup</span>
808
+ <span class="stat-value">${maintenance.lastBackup ? new Date(maintenance.lastBackup).toLocaleString() : 'Never'}</span>
809
+ </div>`;
810
+ } catch (err) {
811
+ console.warn('displayMaintenanceInfo error:', err);
812
+ }
813
+ }
814
+
815
+ function displaySystemHealth(health) {
816
+ // Defensive normalization: /api/health returns { status, checks, uptime, timestamp } (no issues/recommendations)
817
+ // while richer maintenance/system health objects may include arrays. Avoid assuming presence.
818
+ if(!health || typeof health !== 'object') {
819
+ document.getElementById('system-health').innerHTML = '<div class="error-message">Health data unavailable</div>';
820
+ return;
821
+ }
822
+ const normalized = {
823
+ status: (health.status || 'unknown').toString(),
824
+ issues: Array.isArray(health.issues) ? health.issues : [],
825
+ recommendations: Array.isArray(health.recommendations) ? health.recommendations : [],
826
+ uptime: typeof health.uptime === 'number' ? health.uptime : (typeof health.server?.uptime === 'number' ? health.server.uptime : undefined),
827
+ checks: health.checks || {}
828
+ };
829
+ // Inject CPU check if not present using last resource cache / stats
830
+ try {
831
+ if(!normalized.checks.cpu){
832
+ let cpuVal = undefined;
833
+ if(window.__resourceTrendCache && typeof window.__resourceTrendCache.latestCpu === 'number') cpuVal = window.__resourceTrendCache.latestCpu;
834
+ else if(window.lastSystemStats && window.lastSystemStats.cpuUsage && typeof window.lastSystemStats.cpuUsage.percent==='number') cpuVal = window.lastSystemStats.cpuUsage.percent;
835
+ if(typeof cpuVal === 'number'){
836
+ // simple thresholds: <50 ok, 50-80 warn (ok), >80 fail
837
+ normalized.checks.cpu = cpuVal < 85; // treat >85 as fail
838
+ if(cpuVal >=85 && !normalized.issues.some(i=>/high cpu/i.test(i))) normalized.issues.push('High CPU usage');
839
+ }
840
+ }
841
+ // Inject Memory check if not present (heap usage based) using resource trend cache or lastSystemStats
842
+ if(!normalized.checks.memory){
843
+ let memPercent = undefined;
844
+ // Use heapLimit (V8 heap_size_limit) when available for accurate memory health.
845
+ // heapTotal tracks current V8 heap reservation which stays close to heapUsed, causing false alarms.
846
+ const mu = window.lastSystemStats && window.lastSystemStats.memoryUsage;
847
+ if(mu && typeof mu.heapUsed === 'number' && typeof mu.heapLimit === 'number' && mu.heapLimit > 0){
848
+ memPercent = (mu.heapUsed / mu.heapLimit) * 100;
849
+ } else if(mu && typeof mu.heapUsed === 'number' && typeof mu.heapTotal === 'number' && mu.heapTotal > 0){
850
+ memPercent = (mu.heapUsed / mu.heapTotal) * 100;
851
+ }
852
+ if(typeof memPercent === 'number'){
853
+ // Thresholds: <70 ok, 70-90 warn (still ok), >90 fail
854
+ normalized.checks.memory = memPercent < 90;
855
+ if(memPercent >= 90 && !normalized.issues.some(i=>/high memory/i.test(i))) normalized.issues.push('High memory usage');
856
+ // Provide a leak / growth recommendation if increasing trend and above moderate threshold
857
+ if(health.memoryTrend === 'increasing' && memPercent >= 75 && !normalized.recommendations.some(r=>/investigate memory growth/i.test(r))){
858
+ normalized.recommendations.push('Investigate memory growth – potential leak risk');
859
+ }
860
+ }
861
+ }
862
+ } catch{/*ignore*/}
863
+ // Uptime regression styling escalation: if an issue contains 'Uptime regression' force critical indicator
864
+ let statusOverride = normalized.status;
865
+ if (normalized.issues.some(i => /uptime regression/i.test(i))) {
866
+ statusOverride = 'critical';
867
+ }
868
+
869
+ // If stats are unavailable, degrade overall status (but don't alarm) – rely on shared window.statsAvailable
870
+ if (!window.statsAvailable) {
871
+ // degrade to 'unknown' instead of forced failure to avoid alarming UI when stats temporarily missing
872
+ if (!normalized.issues.some(i => /statistics unavailable/i.test(i))) normalized.issues.push('Statistics unavailable');
873
+ if (statusOverride === 'healthy') statusOverride = 'unknown';
874
+ }
875
+ const statusClass = `status-${statusOverride}`;
876
+ let html = `
877
+ <div class="stat-row">
878
+ <span class="stat-label">Overall Status</span>
879
+ <span class="stat-value">
880
+ ${statusOverride.toUpperCase()}
881
+ <span class="${statusClass} status-indicator"></span>
882
+ </span>
883
+ </div>
884
+ `;
885
+
886
+ // Show basic check breakdown if present
887
+ try {
888
+ const checkKeys = Object.keys(normalized.checks);
889
+ if(checkKeys.length){
890
+ html += '<div class="health-mt"><strong>Checks:</strong><ul class="health-list">' +
891
+ checkKeys.map(k => `<li class="${normalized.checks[k]?'text-ok':'text-fail'}">${k}: ${normalized.checks[k]?'ok':'fail'}</li>`).join('') + '</ul></div>';
892
+ }
893
+ } catch { /* ignore */ }
894
+
895
+ // Add CPU trend information if available
896
+ if (health.cpuTrend) {
897
+ html += `
898
+ <div class="health-mt">
899
+ <strong>CPU Trend:</strong>
900
+ <span class="${health.cpuTrend === 'stable' ? 'text-ok' : health.cpuTrend === 'increasing' ? 'text-warn' : 'text-fail'}">
901
+ ${health.cpuTrend}
902
+ </span>
903
+ </div>
904
+ `;
905
+ }
906
+
907
+ // Add memory trend information if available
908
+ if (health.memoryTrend) {
909
+ const formatGrowthRate = (rate) => {
910
+ if (Math.abs(rate) < 1024) return `${rate.toFixed(0)} B/min`;
911
+ if (Math.abs(rate) < 1024 * 1024) return `${(rate / 1024).toFixed(1)} KB/min`;
912
+ return `${(rate / (1024 * 1024)).toFixed(1)} MB/min`;
913
+ };
914
+
915
+ html += `
916
+ <div class="health-mt">
917
+ <strong>Memory Trend:</strong>
918
+ <span class="${health.memoryTrend === 'stable' ? 'text-ok' : health.memoryTrend === 'increasing' ? 'text-warn' : 'text-fail'}">
919
+ ${health.memoryTrend}
920
+ </span>
921
+ ${health.memoryGrowthRate ? ` (${formatGrowthRate(health.memoryGrowthRate)})` : ''}
922
+ </div>
923
+ `;
924
+ }
925
+
926
+ if (normalized.issues.length > 0) {
927
+ html += `
928
+ <div class="health-mt-lg">
929
+ <strong>Issues:</strong>
930
+ <ul class="health-list">
931
+ ${normalized.issues.map(issue => `<li class="text-fail">${issue}</li>`).join('')}
932
+ </ul>
933
+ </div>
934
+ `;
935
+ }
936
+
937
+ if (normalized.recommendations.length > 0) {
938
+ html += `
939
+ <div class="health-mt-lg">
940
+ <strong>Recommendations:</strong>
941
+ <ul class="health-list">
942
+ ${normalized.recommendations.map(rec => `<li class="text-warn">${rec}</li>`).join('')}
943
+ </ul>
944
+ </div>
945
+ `;
946
+ }
947
+
948
+ // Append CPU / Memory spark lines (moved from performance card)
949
+ try {
950
+ if (window.__resourceTrendCache) {
951
+ const t = window.__resourceTrendCache;
952
+ const sparkHtml = `
953
+ <div class="mt-lg">
954
+ <div class="resource-trend-label">Resource Trend</div>
955
+ <div class="resource-trend-col">
956
+ <div class="spark-col">
957
+ <span class="spark-label">CPU Spark (last ${Math.min(40, t.sampleCount || 0)} samples)</span>
958
+ <span class="spark-value">${t.spark || ''}</span>
959
+ <span class="spark-stats">Latest ${t.latestCpu?.toFixed ? t.latestCpu.toFixed(1) : '0'}% • Min ${(t.minCpu??0).toFixed(1)}% • Max ${(t.maxCpu??0).toFixed(1)}%</span>
960
+ </div>
961
+ <div class="spark-col">
962
+ <span class="spark-label">Mem Spark (heap)</span>
963
+ <span class="spark-value">${t.memSpark || ''}</span>
964
+ <span class="spark-stats">Latest ${(t.latestHeap/1024/1024).toFixed(2)} MB • Min ${(t.minHeap/1024/1024).toFixed(2)} MB • Max ${(t.maxHeap/1024/1024).toFixed(2)} MB</span>
965
+ </div>
966
+ </div>
967
+ </div>`;
968
+ html += sparkHtml;
969
+ }
970
+ } catch {/* ignore */}
971
+
972
+ document.getElementById('system-health').innerHTML = html;
973
+ try { window.lastSystemHealth = health; } catch {/* ignore */}
974
+ }
975
+
976
+ // --- Backup / Restore ---
977
+ // Extracted to js/admin.maintenance.js?v=1.19.1-e48e1b0d
978
+
979
+ async function performBackup() {
980
+ try {
981
+ showSuccess('Backup started...');
982
+ const response = await fetch('/api/admin/maintenance/backup', {
983
+ method: 'POST'
984
+ });
985
+
986
+ const data = await response.json();
987
+ if (data.success) {
988
+ showSuccess(`Backup completed: ${data.backupId}`);
989
+ loadMaintenanceStatus();
990
+ } else {
991
+ showError('Backup failed');
992
+ }
993
+ } catch (error) {
994
+ console.error('Error performing backup:', error);
995
+ showError('Backup failed');
996
+ }
997
+ }
998
+
999
+ async function clearCaches() {
1000
+ try {
1001
+ const response = await fetch('/api/admin/cache/clear', {
1002
+ method: 'POST'
1003
+ });
1004
+
1005
+ const data = await response.json();
1006
+ if (data.success) {
1007
+ showSuccess(`Caches cleared: ${data.cleared.join(', ')}`);
1008
+ } else {
1009
+ showError('Failed to clear caches');
1010
+ }
1011
+ } catch (error) {
1012
+ console.error('Error clearing caches:', error);
1013
+ showError('Failed to clear caches');
1014
+ }
1015
+ }
1016
+
1017
+ async function restartServer() {
1018
+ if (!confirm('Are you sure you want to restart the server? This may temporarily interrupt service.')) {
1019
+ return;
1020
+ }
1021
+
1022
+ try {
1023
+ showSuccess('Server restart initiated...');
1024
+ const response = await fetch('/api/admin/restart', {
1025
+ method: 'POST',
1026
+ headers: { 'Content-Type': 'application/json' },
1027
+ body: JSON.stringify({ component: 'all' })
1028
+ });
1029
+
1030
+ const data = await response.json();
1031
+ if (data.success) {
1032
+ showSuccess(data.message);
1033
+ } else {
1034
+ showError('Server restart failed');
1035
+ }
1036
+ } catch (error) {
1037
+ console.error('Error restarting server:', error);
1038
+ showError('Server restart failed');
1039
+ }
1040
+ }
1041
+
1042
+ async function loadConfiguration() {
1043
+ // Primary implementation in js/admin.config.js?v=1.19.1-e48e1b0d (loaded via defer).
1044
+ // This inline fallback only fires if the external script failed to load.
1045
+ if (window.__configExternalLoaded) return;
1046
+ try {
1047
+ var res = await fetch('/api/admin/config');
1048
+ var data = await res.json();
1049
+ if (!data.success) throw new Error('Failed to load config');
1050
+ var cfg = data.config;
1051
+ var html = '<form onsubmit="return updateConfiguration(event)">'
1052
+ + '<div class="form-group"><label class="form-label">Max Connections</label>'
1053
+ + '<input class="form-input" type="number" id="cfg-maxConnections" value="' + cfg.serverSettings.maxConnections + '" /></div>'
1054
+ + '<div class="form-group"><label class="form-label">Request Timeout (ms)</label>'
1055
+ + '<input class="form-input" type="number" id="cfg-requestTimeout" value="' + cfg.serverSettings.requestTimeout + '" /></div>'
1056
+ + '<div class="form-group"><label class="form-label">Verbose Logging</label>'
1057
+ + '<select class="form-input" id="cfg-verbose"><option value="1"' + (cfg.serverSettings.enableVerboseLogging ? ' selected' : '') + '>Enabled</option>'
1058
+ + '<option value="0"' + (!cfg.serverSettings.enableVerboseLogging ? ' selected' : '') + '>Disabled</option></select></div>'
1059
+ + '<div class="form-group"><label class="form-label">Enable Mutation</label>'
1060
+ + '<select class="form-input" id="cfg-mutation"><option value="1"' + (cfg.serverSettings.enableMutation ? ' selected' : '') + '>Enabled</option>'
1061
+ + '<option value="0"' + (!cfg.serverSettings.enableMutation ? ' selected' : '') + '>Disabled</option></select></div>'
1062
+ + '<div style="margin-top:10px;"><button class="action-btn" type="submit">💾 Save Config</button></div>'
1063
+ + '</form>'
1064
+ + '<div style="opacity:0.5; margin-top:12px; font-size:11px;">Inline fallback — external config JS not loaded. Flags panel unavailable.</div>';
1065
+ document.getElementById('config-form').innerHTML = html;
1066
+ } catch (e) {
1067
+ document.getElementById('config-form').innerHTML = '<div class="error">Failed to load configuration</div>';
1068
+ }
1069
+ }
1070
+
1071
+ async function updateConfiguration(ev) {
1072
+ ev.preventDefault();
1073
+ var flagSelects = document.querySelectorAll('[data-flag]');
1074
+ var featureFlags = {};
1075
+ flagSelects.forEach(function(sel){
1076
+ var name = sel.getAttribute('data-flag');
1077
+ if(!name) return;
1078
+ featureFlags[name] = sel.value === '1';
1079
+ });
1080
+ var updates = {
1081
+ serverSettings: {
1082
+ maxConnections: parseInt(document.getElementById('cfg-maxConnections').value),
1083
+ requestTimeout: parseInt(document.getElementById('cfg-requestTimeout').value),
1084
+ enableVerboseLogging: document.getElementById('cfg-verbose').value === '1',
1085
+ enableMutation: document.getElementById('cfg-mutation').value === '1',
1086
+ rateLimit: {
1087
+ windowMs: parseInt((document.getElementById('cfg-windowMs') || {}).value || '60000'),
1088
+ maxRequests: parseInt((document.getElementById('cfg-maxRequests') || {}).value || '100')
1089
+ }
1090
+ },
1091
+ featureFlags: featureFlags
1092
+ };
1093
+ try {
1094
+ var res = await fetch('/api/admin/config', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(updates)});
1095
+ var data = await res.json();
1096
+ if (data.success) { showSuccess('Configuration updated'); loadConfiguration(); } else { showError(data.error || 'Update failed'); }
1097
+ } catch (e) { showError('Update failed'); }
1098
+ return false;
1099
+ }
1100
+
1101
+ // Monitoring functions moved to js/admin.monitor.js?v=1.19.1-e48e1b0d
1102
+
1103
+ // ===== Log Viewer =====
1104
+ // Extracted to js/admin.logs.js?v=1.19.1-e48e1b0d
1105
+
1106
+ // ===== Instruction Management =====
1107
+ let instructionEditing = null;
1108
+ // Pagination state
1109
+ let allInstructions = [];
1110
+ let instructionPage = 1;
1111
+ let instructionPageSize = 25; // default page size
1112
+
1113
+ function getInstructionPageSizes() { return [10,25,50,100, 'All']; }
1114
+
1115
+ function buildInstructionPaginationControls(totalFiltered) {
1116
+ const container = document.getElementById('instruction-pagination');
1117
+ if (!container) return;
1118
+ const total = totalFiltered;
1119
+ const pageSize = instructionPageSize === 'All' ? total : instructionPageSize;
1120
+ const totalPages = pageSize === 0 ? 1 : Math.max(1, Math.ceil(total / pageSize));
1121
+ if (instructionPage > totalPages) instructionPage = totalPages; // clamp
1122
+
1123
+ const disablePrev = instructionPage <= 1;
1124
+ const disableNext = instructionPage >= totalPages;
1125
+
1126
+ const sizeOptions = getInstructionPageSizes().map(s => `<option value="${s}" ${s===instructionPageSize? 'selected':''}>${s}</option>`).join('');
1127
+ container.innerHTML = `
1128
+ <div class="ipag-row">
1129
+ <label class="ipag-label">Page Size:
1130
+ <select id="instruction-page-size" class="form-input ipag-select">${sizeOptions}</select>
1131
+ </label>
1132
+ <div class="ipag-nav">
1133
+ <button class="action-btn" ${disablePrev?'disabled':''}>⏮ First</button>
1134
+ <button class="action-btn" ${disablePrev?'disabled':''}>◀ Prev</button>
1135
+ <span class="ipag-page">Page ${instructionPage} / ${totalPages}</span>
1136
+ <button class="action-btn" ${disableNext?'disabled':''}>Next ▶</button>
1137
+ <button class="action-btn" ${disableNext?'disabled':''}>Last ⏭</button>
1138
+ </div>
1139
+ <span class="ipag-total">Filtered: ${total} total</span>
1140
+ </div>`;
1141
+ const sizeSelect = document.getElementById('instruction-page-size');
1142
+ sizeSelect.onchange = () => {
1143
+ instructionPageSize = sizeSelect.value === 'All' ? 'All' : parseInt(sizeSelect.value,10);
1144
+ instructionPage = 1; // reset to first page when size changes
1145
+ renderInstructionList(allInstructions);
1146
+ };
1147
+ }
1148
+
1149
+ function changeInstructionPage(dir) {
1150
+ const totalFiltered = getFilteredInstructions(allInstructions).length;
1151
+ const pageSizeVal = instructionPageSize === 'All' ? totalFiltered : instructionPageSize;
1152
+ const totalPages = pageSizeVal === 0 ? 1 : Math.max(1, Math.ceil(totalFiltered / pageSizeVal));
1153
+ if (dir === 'first') instructionPage = 1;
1154
+ else if (dir === 'prev' && instructionPage > 1) instructionPage--;
1155
+ else if (dir === 'next' && instructionPage < totalPages) instructionPage++;
1156
+ else if (dir === 'last') instructionPage = totalPages;
1157
+ renderInstructionList(allInstructions);
1158
+ }
1159
+
1160
+ // Instruction filtering & sorting enhancements
1161
+ function getFilteredInstructions(list) {
1162
+ const nameFilter = (document.getElementById('instruction-filter').value || '').toLowerCase();
1163
+ const categoryFilter = (document.getElementById('instruction-category-filter')?.value || '');
1164
+ const sizeFilter = (document.getElementById('instruction-size-filter')?.value || '');
1165
+ let filtered = list.filter(i => i.name.toLowerCase().includes(nameFilter));
1166
+ if (categoryFilter) {
1167
+ filtered = filtered.filter(i => {
1168
+ if (i.category === categoryFilter) return true;
1169
+ if (Array.isArray(i.categories) && i.categories.includes(categoryFilter)) return true;
1170
+ return false;
1171
+ });
1172
+ }
1173
+ if (sizeFilter) filtered = filtered.filter(i => i.sizeCategory === sizeFilter);
1174
+ const sortSelect = document.getElementById('instruction-sort');
1175
+ const sortVal = sortSelect ? sortSelect.value : 'name-asc';
1176
+ const cmp = (a,b, key, dir='asc') => {
1177
+ if (a[key] === b[key]) return 0;
1178
+ return (a[key] < b[key] ? -1 : 1) * (dir === 'asc' ? 1 : -1);
1179
+ };
1180
+ switch(sortVal) {
1181
+ case 'name-desc': filtered.sort((a,b)=>cmp(a,b,'name','desc')); break;
1182
+ case 'size-asc': filtered.sort((a,b)=>cmp(a,b,'size','asc')); break;
1183
+ case 'size-desc': filtered.sort((a,b)=>cmp(a,b,'size','desc')); break;
1184
+ case 'mtime-asc': filtered.sort((a,b)=>cmp(a,b,'mtime','asc')); break;
1185
+ case 'mtime-desc': filtered.sort((a,b)=>cmp(a,b,'mtime','desc')); break;
1186
+ case 'category': filtered.sort((a,b)=>cmp(a,b,'category','asc') || cmp(a,b,'name','asc')); break;
1187
+ default: // name-asc
1188
+ filtered.sort((a,b)=>cmp(a,b,'name','asc'));
1189
+ }
1190
+ return filtered;
1191
+ }
1192
+
1193
+ /* Removed legacy duplicate loadInstructions; unified enhanced version defined later */
1194
+
1195
+ async function loadInstructionCategories() {
1196
+ try {
1197
+ const res = await fetch('/api/instructions/categories');
1198
+ if(!res.ok) throw new Error('http '+res.status);
1199
+ const data = await res.json();
1200
+ // Accept shapes: {success:true,categories:[{name,count}]}, {success:true,data:{categories:[...]}}
1201
+ let cats = data.categories || data.data?.categories || [];
1202
+ if(Array.isArray(cats) && cats.length && typeof cats[0] === 'string') {
1203
+ cats = cats.map(n=>({ name:n, count: undefined }));
1204
+ }
1205
+ if(!Array.isArray(cats)) cats = [];
1206
+ const select = document.getElementById('instruction-category-filter');
1207
+ if(select){
1208
+ select.innerHTML = '<option value="">All Categories</option>';
1209
+ cats.forEach(cat => {
1210
+ if(!cat || !cat.name) return;
1211
+ const option = document.createElement('option');
1212
+ option.value = cat.name;
1213
+ option.textContent = cat.count != null ? `${cat.name} (${cat.count})` : cat.name;
1214
+ select.appendChild(option);
1215
+ });
1216
+ }
1217
+ return cats.map(c=>c.name);
1218
+ } catch (e) {
1219
+ console.warn('Failed to load instruction categories:', e);
1220
+ return [];
1221
+ }
1222
+ }
1223
+
1224
+ function renderInstructionList(instructions) {
1225
+ const filtered = getFilteredInstructions(instructions);
1226
+ if (filtered.length === 0) {
1227
+ document.getElementById('instructions-list').innerHTML = '<p>No instructions found</p>';
1228
+ buildInstructionPaginationControls(0);
1229
+ return;
1230
+ }
1231
+ const totalFiltered = filtered.length;
1232
+ let pageItems = filtered;
1233
+ if (instructionPageSize !== 'All') {
1234
+ const start = (instructionPage - 1) * instructionPageSize;
1235
+ const end = start + instructionPageSize;
1236
+ pageItems = filtered.slice(start, end);
1237
+ }
1238
+ const rows = pageItems.map(instr => {
1239
+ const rawSummary = (instr.semanticSummary || '').trim();
1240
+ let short = rawSummary.slice(0, 160);
1241
+ if (rawSummary.length > 160) short += '…';
1242
+ const safeSummary = escapeHtml(short);
1243
+ return `
1244
+ <div class="session-item instr-item-bg">
1245
+ <div class="session-header">
1246
+ <span class="session-id instr-id-bg">${instr.name}</span>
1247
+ <div>
1248
+ <button class="action-btn" onclick="editInstruction('${instr.name}')">✏ Edit</button>
1249
+ <button class="action-btn danger" onclick="deleteInstruction('${instr.name}')">🗑 Delete</button>
1250
+ </div>
1251
+ </div>
1252
+ <div class="stat-row"><span class="stat-label">Category</span><span class="stat-value">${instr.category || '—'}</span></div>
1253
+ <div class="stat-row"><span class="stat-label">Size</span><span class="stat-value">${instr.size} bytes (${instr.sizeCategory})</span></div>
1254
+ <div class="stat-row"><span class="stat-label">Modified</span><span class="stat-value">${new Date(instr.mtime).toLocaleString()}</span></div>
1255
+ <div class="stat-row stat-row-top">
1256
+ <span class="stat-label stat-label-pt">Summary</span>
1257
+ <span class="stat-value stat-value-wrap">
1258
+ ${safeSummary || '<span style=\"opacity:.5;\">—</span>'}
1259
+ </span>
1260
+ </div>
1261
+ </div>`;
1262
+ }).join('');
1263
+ document.getElementById('instructions-list').innerHTML = rows;
1264
+ buildInstructionPaginationControls(totalFiltered);
1265
+ }
1266
+
1267
+ function filterInstructions() {
1268
+ instructionPage = 1; // reset to first page on filter change
1269
+ renderInstructionList(allInstructions);
1270
+ }
1271
+
1272
+ function showCreateInstruction() {
1273
+ instructionEditing = null;
1274
+ document.getElementById('instruction-editor-title').textContent = 'New Instruction';
1275
+ document.getElementById('instruction-filename').value = '';
1276
+ document.getElementById('instruction-filename').disabled = false;
1277
+ document.getElementById('instruction-content').value = '{\n "description": "New instruction"\n}';
1278
+ ensureInstructionEditorAtTop();
1279
+ const ed = document.getElementById('instruction-editor');
1280
+ ed.classList.remove('hidden');
1281
+ // Smooth scroll to make editor visible at top
1282
+ try { ed.scrollIntoView({ behavior:'smooth', block:'start' }); } catch {}
1283
+ document.getElementById('instruction-filename').focus();
1284
+ instructionOriginalContent = document.getElementById('instruction-content').value;
1285
+ updateInstructionEditorDiagnostics();
1286
+ }
1287
+
1288
+ async function editInstruction(name) {
1289
+ const editor = document.getElementById('instruction-editor');
1290
+ const filenameEl = document.getElementById('instruction-filename');
1291
+ const contentEl = document.getElementById('instruction-content');
1292
+ let attempts = 0;
1293
+ const maxAttempts = 2; // one retry after initial attempt
1294
+ let lastError;
1295
+ while (attempts < maxAttempts) {
1296
+ try {
1297
+ attempts++;
1298
+ // lightweight loading indicator
1299
+ if (contentEl && attempts === 1) {
1300
+ contentEl.value = '// Loading ' + name + '...';
1301
+ }
1302
+ const res = await fetch('/api/instructions/' + encodeURIComponent(name));
1303
+ if (!res.ok) throw new Error('http ' + res.status);
1304
+ const data = await res.json();
1305
+ if (data.success === false && !data.content && !data.data?.content) throw new Error('server reported failure');
1306
+ if (!data.content && data.data?.content) data.content = data.data.content;
1307
+ if (!data.content) throw new Error('missing content');
1308
+ instructionEditing = name;
1309
+ document.getElementById('instruction-editor-title').textContent = 'Edit Instruction: ' + name;
1310
+ filenameEl.value = name;
1311
+ filenameEl.disabled = true;
1312
+ const pretty = JSON.stringify(data.content, null, 2);
1313
+ contentEl.value = pretty;
1314
+ ensureInstructionEditorAtTop();
1315
+ editor.classList.remove('hidden');
1316
+ try { editor.scrollIntoView({ behavior:'smooth', block:'start' }); } catch {}
1317
+ instructionOriginalContent = pretty;
1318
+ updateInstructionEditorDiagnostics();
1319
+ return; // success
1320
+ } catch (e) {
1321
+ lastError = e;
1322
+ // brief delay before retry to allow file system settle or server warm path
1323
+ if (attempts < maxAttempts) {
1324
+ await new Promise(r => setTimeout(r, 120));
1325
+ continue;
1326
+ }
1327
+ }
1328
+ }
1329
+ console.warn('editInstruction failed after retries', lastError);
1330
+ showError('Failed to load instruction');
1331
+ }
1332
+
1333
+ function cancelEditInstruction() {
1334
+ document.getElementById('instruction-editor').classList.add('hidden');
1335
+ const diff = document.getElementById('instruction-diff-container');
1336
+ if(diff) diff.classList.add('hidden');
1337
+ instructionOriginalContent='';
1338
+ }
1339
+
1340
+ // Reposition editor just below the toolbar (before the instruction list) when activated
1341
+ function ensureInstructionEditorAtTop(){
1342
+ try {
1343
+ const editor = document.getElementById('instruction-editor');
1344
+ const list = document.getElementById('instructions-list');
1345
+ if(!editor || !list) return;
1346
+ const parent = list.parentElement; // card body
1347
+ // The toolbar is the element immediately before instructions-list (with buttons + filters)
1348
+ if(parent && parent.contains(list)){
1349
+ // Insert editor right BEFORE instructions-list so it appears at top
1350
+ if(editor.nextElementSibling !== list){
1351
+ parent.insertBefore(editor, list);
1352
+ }
1353
+ }
1354
+ } catch {/* ignore */}
1355
+ }
1356
+
1357
+ // --- Instruction Editor Helpers (added patch) ---
1358
+ let instructionOriginalContent = '';
1359
+ let instructionDiffVisible = false;
1360
+
1361
+ function safeParseInstruction(raw){
1362
+ try { return JSON.parse(raw); } catch { return null; }
1363
+ }
1364
+
1365
+ function updateInstructionEditorDiagnostics(){
1366
+ const ta = document.getElementById('instruction-content');
1367
+ const diag = document.getElementById('instruction-diagnostics');
1368
+ if(!ta||!diag) return;
1369
+ const raw = ta.value;
1370
+ if(!raw.trim()){ diag.innerHTML = '<em>Empty.</em>'; return; }
1371
+ const parsed = safeParseInstruction(raw);
1372
+ if(!parsed){
1373
+ diag.innerHTML = '<span class="text-danger">Invalid JSON</span>';
1374
+ } else {
1375
+ const size = raw.length;
1376
+ const cats = Array.isArray(parsed.categories)? parsed.categories.length : 0;
1377
+ const schemaVer = parsed.schemaVersion || parsed.schema || '?';
1378
+ const changed = instructionOriginalContent && raw !== instructionOriginalContent;
1379
+ diag.innerHTML = `Size: ${size} chars • Categories: ${cats} • Schema: ${schemaVer} ${changed?'<span class="text-warn">(modified)</span>':''}`;
1380
+ }
1381
+ if(instructionDiffVisible) refreshInstructionDiff();
1382
+ }
1383
+
1384
+ function refreshInstructionDiff(){
1385
+ const diffWrap = document.getElementById('instruction-diff-container');
1386
+ const diffPre = document.getElementById('instruction-diff');
1387
+ const ta = document.getElementById('instruction-content');
1388
+ if(!diffWrap||!diffPre||!ta) return;
1389
+ if(!instructionOriginalContent){ diffPre.textContent='(no baseline)'; return; }
1390
+ if(ta.value === instructionOriginalContent){ diffPre.textContent='(no changes)'; return; }
1391
+ // Simple line diff (not optimal but lightweight)
1392
+ const before = instructionOriginalContent.split(/\r?\n/);
1393
+ const after = ta.value.split(/\r?\n/);
1394
+ const max = Math.max(before.length, after.length);
1395
+ const out = [];
1396
+ for(let i=0;i<max;i++){
1397
+ const a = before[i];
1398
+ const b = after[i];
1399
+ if(a === b){
1400
+ if(a !== undefined) out.push(' ' + a);
1401
+ } else {
1402
+ if(a !== undefined) out.push('- ' + a);
1403
+ if(b !== undefined) out.push('+ ' + b);
1404
+ }
1405
+ }
1406
+ diffPre.textContent = out.join('\n');
1407
+ }
1408
+
1409
+ function toggleInstructionDiff(){
1410
+ instructionDiffVisible = !instructionDiffVisible;
1411
+ const wrap = document.getElementById('instruction-diff-container');
1412
+ if(!wrap) return;
1413
+ if(instructionDiffVisible){
1414
+ wrap.classList.remove('hidden');
1415
+ refreshInstructionDiff();
1416
+ } else {
1417
+ wrap.classList.add('hidden');
1418
+ }
1419
+ }
1420
+
1421
+ async function saveInstruction(){
1422
+ const nameEl = document.getElementById('instruction-filename');
1423
+ const ta = document.getElementById('instruction-content');
1424
+ if(!nameEl||!ta) return;
1425
+ const raw = ta.value;
1426
+ const parsed = safeParseInstruction(raw);
1427
+ if(!parsed){ showError('Cannot save: invalid JSON'); return; }
1428
+ // Auto-upgrade template schemaVersion if legacy 1.x
1429
+ if(parsed && parsed.schemaVersion && /^1(\.|$)/.test(String(parsed.schemaVersion))){
1430
+ parsed.schemaVersion = '2';
1431
+ }
1432
+ const body = { content: parsed };
1433
+ let url = '/api/instructions';
1434
+ let method = 'POST';
1435
+ if(instructionEditing){
1436
+ url += '/' + encodeURIComponent(instructionEditing);
1437
+ method = 'PUT';
1438
+ } else {
1439
+ body.name = nameEl.value.trim();
1440
+ if(!body.name){ showError('Provide file name'); return; }
1441
+ }
1442
+ try {
1443
+ const res = await fetch(url, { method, headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
1444
+ const data = await res.json();
1445
+ if(!res.ok || !data.success){ throw new Error(data.error || data.message || 'Save failed'); }
1446
+ showSuccess(instructionEditing? 'Instruction updated':'Instruction created');
1447
+ instructionOriginalContent = JSON.stringify(parsed, null, 2);
1448
+ ta.value = instructionOriginalContent; // normalized pretty
1449
+ if(!instructionEditing) instructionEditing = body.name;
1450
+ updateInstructionEditorDiagnostics();
1451
+ loadInstructions();
1452
+ } catch(e){ showError(e.message || 'Save failed'); }
1453
+ }
1454
+
1455
+ async function loadInstructions() {
1456
+ const listEl = document.getElementById('instructions-list');
1457
+ listEl.innerHTML = 'Loading...';
1458
+ try {
1459
+ // Load categories first to populate the dropdown (ignore errors; we'll derive later if needed)
1460
+ const catNames = await loadInstructionCategories();
1461
+ const res = await fetch('/api/instructions');
1462
+ if(!res.ok) throw new Error('http '+res.status);
1463
+ const data = await res.json();
1464
+ // Accept shapes: {success:true,instructions:[...]}, {data:{instructions:[...]}}, or legacy {instructions:[...]}
1465
+ if (!('success' in data) && !('data' in data) && !('instructions' in data)) throw new Error('unrecognized instructions payload');
1466
+ const rawList = data.instructions || data.data?.instructions || [];
1467
+ allInstructions = Array.isArray(rawList) ? rawList : [];
1468
+ try { console.log('[dashboard] fetched instructions:', allInstructions.length); } catch {}
1469
+ // If categories endpoint failed / returned empty, derive from instruction list
1470
+ if(!catNames.length) {
1471
+ try {
1472
+ const select = document.getElementById('instruction-category-filter');
1473
+ if(select){
1474
+ const derived = Array.from(new Set(allInstructions.flatMap(i=> [i.category, ...(Array.isArray(i.categories)? i.categories: [])]).filter(Boolean))).sort();
1475
+ derived.forEach(n=>{
1476
+ const opt = document.createElement('option');
1477
+ opt.value = n; opt.textContent = n; select.appendChild(opt);
1478
+ });
1479
+ }
1480
+ } catch(_) { /* ignore */ }
1481
+ }
1482
+ instructionPage = 1; // reset page whenever we reload
1483
+ renderInstructionList(allInstructions);
1484
+ } catch (e) {
1485
+ console.warn('loadInstructions error', e);
1486
+ listEl.innerHTML = '<div class="error">Failed to load instructions</div>';
1487
+ }
1488
+ }
1489
+
1490
+ // Re-introduced after patch: formatInstructionJson utility (was accidentally truncated)
1491
+ function formatInstructionJson(){
1492
+ const ta = document.getElementById('instruction-content');
1493
+ if(!ta) return;
1494
+ try {
1495
+ const parsed = JSON.parse(ta.value);
1496
+ ta.value = JSON.stringify(parsed, null, 2);
1497
+ updateInstructionEditorDiagnostics();
1498
+ } catch { showError('Cannot format: invalid JSON'); }
1499
+ }
1500
+
1501
+ function applyInstructionTemplate(){
1502
+ const ta = document.getElementById('instruction-content');
1503
+ if(!ta) return;
1504
+ if(ta.value.trim() && !confirm('Replace current content with template?')) return;
1505
+ const now = new Date().toISOString();
1506
+ const template = {
1507
+ id: 'sample-instruction',
1508
+ title: 'Sample Instruction',
1509
+ body: 'Detailed instruction content here.\nAdd multi-line guidance and steps.',
1510
+ priority: 50,
1511
+ audience: 'all',
1512
+ requirement: 'optional',
1513
+ categories: ['general'],
1514
+ primaryCategory: 'general',
1515
+ reviewIntervalDays: 180,
1516
+ schemaVersion: '4',
1517
+ description: 'Describe purpose and scope.',
1518
+ createdAt: now,
1519
+ updatedAt: now
1520
+ };
1521
+ ta.value = JSON.stringify(template, null, 2);
1522
+ updateInstructionEditorDiagnostics();
1523
+ }
1524
+
1525
+ // ===== Resource Trend (CPU/Mem) Long-Term History (merged into Performance card) =====
1526
+ (function initResourceTrendMerge(){
1527
+ async function fetchResourceTrends(){
1528
+ try {
1529
+ const res = await fetch('/api/system/resources?limit=300');
1530
+ if(!res.ok) throw new Error('http '+res.status);
1531
+ const json = await res.json();
1532
+ const samples = json?.data?.samples || [];
1533
+ const trend = json?.data?.trend || { cpuSlope:0, memSlope:0 };
1534
+ if(samples.length === 0){
1535
+ window.__resourceTrendCache = { windowSec:0, sampleCount:0, latestCpu:0, latestHeap:0, cpuSlope:0, memSlope:0, spark:'' };
1536
+ // trigger re-render next stats update
1537
+ return;
1538
+ }
1539
+ const latest = samples[samples.length-1];
1540
+ const first = samples[0];
1541
+ const durationSec = ((latest.timestamp - first.timestamp)/1000).toFixed(0);
1542
+ const tail = samples.slice(-40);
1543
+ const spark = tail.map(s=>{
1544
+ const v = Math.min(100, Math.max(0, s.cpuPercent));
1545
+ const idx = Math.round(v/12.5);
1546
+ const blocks = ['▁','▂','▃','▄','▅','▆','▇','█'];
1547
+ return blocks[Math.min(blocks.length-1, idx)];
1548
+ }).join('');
1549
+ const minCpu = tail.reduce((m,s)=> s.cpuPercent<m? s.cpuPercent:m, tail[0].cpuPercent);
1550
+ const maxCpu = tail.reduce((m,s)=> s.cpuPercent>m? s.cpuPercent:m, tail[0].cpuPercent);
1551
+ // Memory spark (scale heap delta relative to max in window)
1552
+ const maxHeap = tail.reduce((m,s)=> s.heapUsed>m?s.heapUsed:m,0) || 1;
1553
+ const minHeap = tail.reduce((m,s)=> s.heapUsed<m?s.heapUsed:m, tail[0].heapUsed);
1554
+ const memSpark = tail.map(s=>{
1555
+ const ratio = Math.min(1, Math.max(0, s.heapUsed / maxHeap));
1556
+ const idx = Math.round(ratio*7);
1557
+ const blocks = ['▁','▂','▃','▄','▅','▆','▇','█'];
1558
+ return blocks[Math.min(blocks.length-1, idx)];
1559
+ }).join('');
1560
+ window.__resourceTrendCache = {
1561
+ windowSec: durationSec,
1562
+ sampleCount: samples.length,
1563
+ latestCpu: latest.cpuPercent,
1564
+ latestHeap: latest.heapUsed,
1565
+ minCpu,
1566
+ maxCpu,
1567
+ minHeap,
1568
+ maxHeap,
1569
+ cpuSlope: trend.cpuSlope || 0,
1570
+ memSlope: trend.memSlope || 0,
1571
+ spark,
1572
+ memSpark
1573
+ };
1574
+ // Re-render performance card if stats already loaded
1575
+ try {
1576
+ if(typeof window.lastSystemStats === 'object') displaySystemStats(window.lastSystemStats);
1577
+ if(typeof window.lastSystemHealth === 'object') displaySystemHealth(window.lastSystemHealth);
1578
+ } catch(e){/*ignore*/}
1579
+ } catch(e){
1580
+ // ignore failures; card will just show base stats
1581
+ }
1582
+ }
1583
+ // periodic fetch
1584
+ fetchResourceTrends();
1585
+ setInterval(fetchResourceTrends, 10000);
1586
+ })();
1587
+
1588
+ // Instruction management logic extracted to js/admin.instructions.js?v=1.19.1-e48e1b0d
1589
+ // Functions exposed globally: loadInstructions, renderInstructionList, editInstruction, saveInstruction, deleteInstruction, etc.
1590
+
1591
+ function startAutoRefresh() {
1592
+ refreshInterval = setInterval(() => {
1593
+ if (currentSection === 'overview') {
1594
+ loadOverviewData();
1595
+ } else if (currentSection === 'sessions') {
1596
+ loadSessions();
1597
+ } else if (currentSection === 'maintenance') {
1598
+ loadMaintenanceStatus();
1599
+ }
1600
+ }, 30000); // Refresh every 30 seconds
1601
+ }
1602
+ // Build metadata loader
1603
+ (async function fetchBuildMeta(){
1604
+ try {
1605
+ // Cache bust query param to avoid any intermediary caching of status response
1606
+ const r = await fetch('/api/status?t=' + Date.now());
1607
+ const j = await r.json();
1608
+ const el = document.getElementById('buildMeta');
1609
+ const ver = j.version || '?.?.?';
1610
+ const commit = j.build ? `<span class=\"build-badge\">${j.build}</span>` : '';
1611
+ const bt = j.buildTime ? new Date(j.buildTime).toLocaleString() : 'unknown';
1612
+ el.innerHTML = `Version <strong>${ver}</strong> ${commit} • Built ${bt}`;
1613
+ } catch { const el = document.getElementById('buildMeta'); if(el) el.textContent='Build metadata unavailable'; }
1614
+ })();
1615
+
1616
+ function showError(message) {
1617
+ // Remove existing notifications (only toast notifications, not styled buttons)
1618
+ document.querySelectorAll('.toast-notification').forEach(el => el.remove());
1619
+
1620
+ const errorDiv = document.createElement('div');
1621
+ errorDiv.className = 'error toast-notification';
1622
+ errorDiv.textContent = message;
1623
+ document.querySelector('.admin-container').insertBefore(errorDiv, document.querySelector('.admin-container').firstChild.nextSibling);
1624
+
1625
+ setTimeout(() => errorDiv.remove(), 5000);
1626
+ }
1627
+
1628
+ function showSuccess(message) {
1629
+ // Remove existing notifications (only toast notifications, not styled buttons)
1630
+ document.querySelectorAll('.toast-notification').forEach(el => el.remove());
1631
+
1632
+ const successDiv = document.createElement('div');
1633
+ successDiv.className = 'success toast-notification';
1634
+ successDiv.textContent = message;
1635
+ document.querySelector('.admin-container').insertBefore(successDiv, document.querySelector('.admin-container').firstChild.nextSibling);
1636
+
1637
+ setTimeout(() => successDiv.remove(), 5000);
1638
+ }
1639
+
1640
+ // Utility functions
1641
+ function formatUptime(seconds) {
1642
+ const days = Math.floor(seconds / 86400);
1643
+ const hours = Math.floor((seconds % 86400) / 3600);
1644
+ const minutes = Math.floor((seconds % 3600) / 60);
1645
+
1646
+ if (days > 0) return `${days}d ${hours}h ${minutes}m`;
1647
+ if (hours > 0) return `${hours}h ${minutes}m`;
1648
+ return `${minutes}m`;
1649
+ }
1650
+
1651
+ function formatBytes(bytes) {
1652
+ const sizes = ['B', 'KB', 'MB', 'GB'];
1653
+ if (bytes === 0) return '0 B';
1654
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
1655
+ return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
1656
+ }
1657
+ function escapeHtml(str) {
1658
+ if (str == null) return '';
1659
+ return String(str)
1660
+ .replace(/&/g, '&amp;')
1661
+ .replace(/</g, '&lt;')
1662
+ .replace(/>/g, '&gt;')
1663
+ .replace(/"/g, '&quot;')
1664
+ .replace(/'/g, '&#39;');
1665
+ }
1666
+ // ---------------- Drilldown Layered SVG (Experimental) -----------------
1667
+ // ELK auto-layout integration (Option A): dynamically load elkjs for layered layout.
1668
+ // Falls back to prior manual lane layout if ELK load fails or toggle disabled.
1669
+ const ELK_LOCAL_URL = '/js/elk.bundled.js';
1670
+ let __elkLoading = null; let __elkInstance = null;
1671
+ function ensureElk(){
1672
+ if(__elkInstance) return Promise.resolve(__elkInstance);
1673
+ if(__elkLoading) return __elkLoading;
1674
+ __elkLoading = new Promise((resolve,reject)=>{
1675
+ if(window.ELK){ try { __elkInstance = new window.ELK(); return resolve(__elkInstance); } catch(e){ return reject(e);} }
1676
+ const s = document.createElement('script');
1677
+ s.src = ELK_LOCAL_URL; s.async = true; s.onload = ()=>{ try { __elkInstance = new window.ELK(); resolve(__elkInstance);} catch(e){ reject(e);} };
1678
+ s.onerror = (e)=> reject(new Error('elk load failed'));
1679
+ document.head.appendChild(s);
1680
+ });
1681
+ return __elkLoading;
1682
+ }
1683
+ function drilldownUseElk(){
1684
+ const cb = document.getElementById('drill-use-elk');
1685
+ return !!(cb && cb.checked);
1686
+ }
1687
+ async function refreshDrillCategories(){
1688
+ const status = document.getElementById('drill-status');
1689
+ const sel = document.getElementById('drill-categories');
1690
+ if(status) status.textContent='loading categories...';
1691
+ try {
1692
+ const r = await fetch('/api/graph/categories');
1693
+ const j = await r.json();
1694
+ if(!j.success) throw new Error(j.error||'failed');
1695
+ if(sel){
1696
+ sel.innerHTML='';
1697
+ j.categories.forEach(c=>{
1698
+ const o = document.createElement('option');
1699
+ o.value = c.id; o.textContent = `${c.id} (${c.count})`;
1700
+ sel.appendChild(o);
1701
+ });
1702
+ }
1703
+ if(status) status.textContent=`loaded ${j.total} categories`;
1704
+ } catch(e){ if(status) status.textContent='category load error: '+ (e?.message||e); }
1705
+ }
1706
+ async function loadDrillInstructions(){
1707
+ const catSel = document.getElementById('drill-categories');
1708
+ const instSel = document.getElementById('drill-instructions');
1709
+ const status = document.getElementById('drill-status');
1710
+ if(!catSel || !instSel) return;
1711
+ const cats = [...catSel.selectedOptions].map(o=> o.value).join(',');
1712
+ if(status) status.textContent='loading instructions...';
1713
+ try {
1714
+ const r = await fetch('/api/graph/instructions?categories=' + encodeURIComponent(cats));
1715
+ const j = await r.json();
1716
+ if(!j.success) throw new Error(j.error||'failed');
1717
+ instSel.innerHTML='';
1718
+ j.instructions.forEach(i=>{
1719
+ const o = document.createElement('option');
1720
+ o.value = i.id; o.textContent = i.id; instSel.appendChild(o);
1721
+ });
1722
+ if(status) status.textContent=`loaded ${j.count} instructions`;
1723
+ } catch(e){ if(status) status.textContent='instruction load error: '+ (e?.message||e); }
1724
+ }
1725
+ function clearDrillSvg(){
1726
+ const svg = document.getElementById('drill-svg');
1727
+ const status = document.getElementById('drill-status');
1728
+ const catSel = document.getElementById('drill-categories');
1729
+ const instSel = document.getElementById('drill-instructions');
1730
+
1731
+ if(svg) svg.innerHTML='';
1732
+ if(status) status.textContent='selections cleared';
1733
+
1734
+ // Clear selections in both dropdowns
1735
+ if(catSel) {
1736
+ for(let i = 0; i < catSel.options.length; i++) {
1737
+ catSel.options[i].selected = false;
1738
+ }
1739
+ }
1740
+ if(instSel) {
1741
+ for(let i = 0; i < instSel.options.length; i++) {
1742
+ instSel.options[i].selected = false;
1743
+ }
1744
+ }
1745
+ }
1746
+ async function renderDrillSvg(){
1747
+ const instSel = document.getElementById('drill-instructions');
1748
+ const status = document.getElementById('drill-status');
1749
+ const svg = document.getElementById('drill-svg');
1750
+ if(!instSel || !svg) return;
1751
+ // Clear detail panel each render
1752
+ const detail = document.getElementById('drill-detail');
1753
+ if(detail){ detail.style.display='none'; detail.textContent=''; }
1754
+ // Persistent for click handlers
1755
+ window.__lastDrillData = null;
1756
+ const ids = [...instSel.selectedOptions].map(o=> o.value);
1757
+ if(!ids.length){ if(status) status.textContent='select instructions first'; return; }
1758
+ const expandEnabled = !!document.getElementById('drill-expand') && document.getElementById('drill-expand').checked;
1759
+ const membershipEnabled = !!document.getElementById('drill-show-membership') && document.getElementById('drill-show-membership').checked;
1760
+ if(status) status.textContent='fetching relations...';
1761
+ try {
1762
+ const r = await fetch('/api/graph/relations?instructions=' + encodeURIComponent(ids.join(',')) + (expandEnabled? '&expand=1':'') );
1763
+ const j = await r.json();
1764
+ if(!j.success) throw new Error(j.error||'failed');
1765
+ const instructions = j.nodes.filter(n=> n.nodeType==='instruction');
1766
+ // Keep full structure for interactions
1767
+ window.__lastDrillData = { raw:j, instructions };
1768
+ const nodeWidth = 140; const nodeHeight = 32;
1769
+ // Decide layout strategy
1770
+ let usedElk = false;
1771
+ let positions = new Map();
1772
+ let categories = [];
1773
+ let instrEdgeCount = 0; // unified edge count for legend
1774
+ if(drilldownUseElk()){
1775
+ if(status) status.textContent='loading ELK layout...';
1776
+ try {
1777
+ const elk = await ensureElk();
1778
+ if(!elk) throw new Error('ELK unavailable');
1779
+ const elkGraph = {
1780
+ id:'root',
1781
+ layoutOptions: {
1782
+ 'elk.algorithm':'layered',
1783
+ 'elk.direction':'RIGHT',
1784
+ 'elk.layered.spacing.nodeNodeBetweenLayers':'55',
1785
+ 'elk.spacing.nodeNode':'30'
1786
+ },
1787
+ children: instructions.map(inst=> ({ id: inst.id, width: nodeWidth, height: nodeHeight })),
1788
+ edges: j.edges.filter(e=> e.type!=='category' && instructions.find(n=> n.id===e.from) && instructions.find(n=> n.id===e.to)).map((e,i)=> ({ id:'e'+i, sources:[e.from], targets:[e.to] }))
1789
+ };
1790
+ const laidOut = await elk.layout(elkGraph);
1791
+ // Initial raw positions from ELK
1792
+ laidOut.children.forEach(c=>{ positions.set(c.id,{ x:c.x||0, y:c.y||0 }); });
1793
+ usedElk = true;
1794
+ // Derive categories (primary only for now)
1795
+ categories = [...new Set(instructions.map(i=> (i.categories&&i.categories[0])||'uncategorized'))];
1796
+ // Order categories by average original y so visual order is stable / data-driven
1797
+ categories.sort((a,b)=>{
1798
+ const ay = avg(instructions.filter(i=> (i.categories&&i.categories[0])===a).map(i=> positions.get(i.id)?.y||0));
1799
+ const by = avg(instructions.filter(i=> (i.categories&&i.categories[0])===b).map(i=> positions.get(i.id)?.y||0));
1800
+ return ay - by;
1801
+ });
1802
+ // Snap categories into uniform horizontal lanes for readability
1803
+ const laneHeight = 70; // vertical space per category lane
1804
+ const gutterWidth = 140; // reserved space on left for labels + connectors
1805
+ const padX = 40; const padY = 40; // outer vertical padding + x padding after gutter
1806
+ const categoryLaneY = new Map();
1807
+ categories.forEach((c,i)=> categoryLaneY.set(c, padY + i*laneHeight));
1808
+ // Reposition nodes: keep original x, quantize y to lane + small intra-lane spread based on original y ordering
1809
+ const laneOffsets = new Map();
1810
+ instructions.forEach(inst=>{
1811
+ const cat = (inst.categories&&inst.categories[0])||'uncategorized';
1812
+ const baseY = categoryLaneY.get(cat) || padY;
1813
+ const p = positions.get(inst.id); if(!p) return;
1814
+ p.y = baseY; // snap exactly
1815
+ // shift x by gutter + padX so nodes never overlap labels
1816
+ p.x = p.x + gutterWidth + padX;
1817
+ });
1818
+ // Compute new bounding box after snapping
1819
+ let maxX=0,maxY=0,minX=Infinity,minY=Infinity;
1820
+ positions.forEach(p=>{ maxX=Math.max(maxX,p.x+nodeWidth); maxY=Math.max(maxY,p.y+nodeHeight); minX=Math.min(minX,p.x); minY=Math.min(minY,p.y); });
1821
+ const totalWidth = Math.max(900, maxX - minX + padX*2 + gutterWidth);
1822
+ const totalHeight = Math.max(160, (categories.length? (padY + categories.length*laneHeight) : (maxY-minY+padY*2)));
1823
+ svg.setAttribute('width', String(totalWidth));
1824
+ svg.setAttribute('height', String(totalHeight));
1825
+ // Prepare drawing surface
1826
+ svg.innerHTML='';
1827
+ // Lane backgrounds (subtle)
1828
+ categories.forEach((cat,i)=>{
1829
+ const y = categoryLaneY.get(cat);
1830
+ if(y==null) return;
1831
+ const laneRect = document.createElementNS('http://www.w3.org/2000/svg','rect');
1832
+ laneRect.setAttribute('x', String(gutterWidth - 8)); laneRect.setAttribute('y', String(y - 10));
1833
+ laneRect.setAttribute('width', String(totalWidth - (gutterWidth - 8))); laneRect.setAttribute('height', String(laneHeight));
1834
+ laneRect.setAttribute('fill', i % 2 === 0 ? '#1b2734' : '#1d2c3b'); laneRect.setAttribute('opacity','0.55');
1835
+ svg.appendChild(laneRect);
1836
+ });
1837
+ // Gutter separator line
1838
+ const gutterSep = document.createElementNS('http://www.w3.org/2000/svg','line');
1839
+ gutterSep.setAttribute('x1', String(gutterWidth - 12)); gutterSep.setAttribute('x2', String(gutterWidth - 12));
1840
+ gutterSep.setAttribute('y1','0'); gutterSep.setAttribute('y2', String(totalHeight));
1841
+ gutterSep.setAttribute('stroke','#253548'); gutterSep.setAttribute('stroke-width','1'); gutterSep.setAttribute('opacity','0.9');
1842
+ svg.appendChild(gutterSep);
1843
+ // Vertical guideline (optional future: grid) -- skipped for now
1844
+ // Continue with edges & nodes below
1845
+ // NOTE: status text updated later includes '(ELK snapped)'
1846
+ function avg(arr){ return arr.length? arr.reduce((s,v)=> s+v,0)/arr.length : 0; }
1847
+ // Draw edges from elkGraph edges using node centers (after snapping)
1848
+ const edgeGroup = document.createElementNS('http://www.w3.org/2000/svg','g');
1849
+ edgeGroup.setAttribute('stroke','#5479ff'); edgeGroup.setAttribute('stroke-width','1.1'); edgeGroup.setAttribute('fill','none'); edgeGroup.setAttribute('opacity','0.9');
1850
+ elkGraph.edges.forEach(e=>{
1851
+ const a = positions.get(e.sources[0]); const b = positions.get(e.targets[0]); if(!a||!b) return;
1852
+ const path = document.createElementNS('http://www.w3.org/2000/svg','path');
1853
+ const hdx = Math.max(40, (b.x - a.x)/3);
1854
+ const d = `M ${a.x + nodeWidth} ${a.y + nodeHeight/2} C ${a.x + nodeWidth + hdx} ${a.y + nodeHeight/2}, ${b.x - hdx} ${b.y + nodeHeight/2}, ${b.x} ${b.y + nodeHeight/2}`;
1855
+ path.setAttribute('d', d);
1856
+ edgeGroup.appendChild(path);
1857
+ instrEdgeCount++;
1858
+ });
1859
+ svg.appendChild(edgeGroup);
1860
+ // Arrowhead marker definition (once)
1861
+ const defs = document.createElementNS('http://www.w3.org/2000/svg','defs');
1862
+ const marker = document.createElementNS('http://www.w3.org/2000/svg','marker');
1863
+ marker.setAttribute('id','drill-arrow'); marker.setAttribute('markerWidth','10'); marker.setAttribute('markerHeight','6'); marker.setAttribute('refX','10'); marker.setAttribute('refY','3'); marker.setAttribute('orient','auto');
1864
+ const mpath = document.createElementNS('http://www.w3.org/2000/svg','path'); mpath.setAttribute('d','M0,0 L10,3 L0,6 Z'); mpath.setAttribute('fill','#5479ff'); marker.appendChild(mpath); defs.appendChild(marker); svg.appendChild(defs);
1865
+ edgeGroup.setAttribute('marker-end','url(#drill-arrow)');
1866
+ // Category membership connectors (pseudo category anchors in gutter)
1867
+ if(membershipEnabled){
1868
+ const membershipGroup = document.createElementNS('http://www.w3.org/2000/svg','g');
1869
+ membershipGroup.setAttribute('stroke','#44566d'); membershipGroup.setAttribute('stroke-width','1'); membershipGroup.setAttribute('opacity','0.8');
1870
+ membershipGroup.setAttribute('data-role','membership-group');
1871
+ instructions.forEach(inst=>{
1872
+ const p = positions.get(inst.id); if(!p) return;
1873
+ const yMid = p.y + nodeHeight/2;
1874
+ const startX = gutterWidth - 15; const endX = p.x; // horizontal connector
1875
+ const conn = document.createElementNS('http://www.w3.org/2000/svg','path');
1876
+ conn.setAttribute('d', `M ${startX} ${yMid} L ${endX} ${yMid}`);
1877
+ membershipGroup.appendChild(conn);
1878
+ });
1879
+ svg.appendChild(membershipGroup);
1880
+ }
1881
+ // Nodes (interactive)
1882
+ instructions.forEach(inst=>{
1883
+ const pos = positions.get(inst.id); if(!pos) return;
1884
+ const g = document.createElementNS('http://www.w3.org/2000/svg','g');
1885
+ g.setAttribute('transform', `translate(${pos.x},${pos.y})`);
1886
+ g.setAttribute('data-id', inst.id);
1887
+ g.setAttribute('data-cat', (inst.categories&&inst.categories[0])||'uncategorized');
1888
+ g.style.cursor='pointer';
1889
+ const rect = document.createElementNS('http://www.w3.org/2000/svg','rect');
1890
+ rect.setAttribute('width', String(nodeWidth)); rect.setAttribute('height', String(nodeHeight)); rect.setAttribute('rx','6');
1891
+ rect.setAttribute('fill','#3a4554'); rect.setAttribute('stroke','#6b8cff');
1892
+ rect.classList.add('drill-node-rect');
1893
+ const label = document.createElementNS('http://www.w3.org/2000/svg','text');
1894
+ label.setAttribute('x','8'); label.setAttribute('y','20'); label.setAttribute('fill','#e3ebf5'); label.textContent = inst.id;
1895
+ label.setAttribute('pointer-events','none');
1896
+ // Tooltip
1897
+ const title = document.createElementNS('http://www.w3.org/2000/svg','title'); title.textContent = inst.id; g.appendChild(title);
1898
+ g.appendChild(rect); g.appendChild(label); svg.appendChild(g);
1899
+ g.addEventListener('mouseenter', ()=> rect.setAttribute('stroke','#8fb2ff'));
1900
+ g.addEventListener('mouseleave', ()=>{ if(!g.classList.contains('selected')) rect.setAttribute('stroke','#6b8cff'); });
1901
+ g.addEventListener('click', ()=> selectDrillNode(inst.id));
1902
+ });
1903
+ // Category labels (lane centered)
1904
+ categories.forEach((cat,i)=>{
1905
+ const yCenter = (categoryLaneY.get(cat)||0) + (laneHeight/2) - 10; // adjust for padding applied earlier
1906
+ const t = document.createElementNS('http://www.w3.org/2000/svg','text');
1907
+ t.setAttribute('x','10'); t.setAttribute('y', String(yCenter)); t.setAttribute('fill','#9fb5cc'); t.setAttribute('font-weight','600'); t.textContent = cat; svg.appendChild(t);
1908
+ // Anchor circle for filtering
1909
+ const anchor = document.createElementNS('http://www.w3.org/2000/svg','circle');
1910
+ anchor.setAttribute('cx', String(110)); // inside gutter
1911
+ anchor.setAttribute('cy', String(yCenter-2));
1912
+ anchor.setAttribute('r','6');
1913
+ anchor.setAttribute('fill','#2e3d50');
1914
+ anchor.setAttribute('stroke','#6b8cff');
1915
+ anchor.setAttribute('data-cat-anchor', cat);
1916
+ anchor.style.cursor='pointer';
1917
+ anchor.addEventListener('click', ()=> toggleLaneFilter(cat));
1918
+ const anchorTitle = document.createElementNS('http://www.w3.org/2000/svg','title'); anchorTitle.textContent = 'Filter lane: '+cat; anchor.appendChild(anchorTitle);
1919
+ svg.appendChild(anchor);
1920
+ });
1921
+ if(status) status.textContent = `rendered ${instructions.length} nodes (ELK snapped) - categories ${categories.length}`;
1922
+ } catch(elkErr){ if(status) status.textContent = 'ELK layout failed; using manual lanes (' + (elkErr?.message||elkErr) + ')'; }
1923
+ }
1924
+ if(!usedElk){
1925
+ // Manual lane fallback (original implementation)
1926
+ const categoriesManual = [...new Set(instructions.map(i=> (i.categories&&i.categories[0])||'uncategorized'))].sort();
1927
+ const catIndex = new Map(categoriesManual.map((c,i)=> [c,i]));
1928
+ const laneHeight = 70; const marginLeft = 180; const hGap = 40; const vPad = 24;
1929
+ const laneCounters = new Map(categoriesManual.map(c=> [c,0]));
1930
+ positions = new Map();
1931
+ instructions.forEach(inst=>{
1932
+ const lane = (inst.categories&&inst.categories[0])||'uncategorized';
1933
+ const idx = laneCounters.get(lane) || 0;
1934
+ laneCounters.set(lane, idx+1);
1935
+ const x = marginLeft + idx*(nodeWidth + hGap);
1936
+ const y = vPad + (catIndex.get(lane)||0)*laneHeight;
1937
+ positions.set(inst.id, { x, y });
1938
+ });
1939
+ const totalWidth = Math.max(800, marginLeft + Math.max(1, ...[...laneCounters.values()])* (nodeWidth + hGap));
1940
+ const totalHeight = vPad + categoriesManual.length * laneHeight;
1941
+ svg.setAttribute('width', String(totalWidth));
1942
+ svg.setAttribute('height', String(totalHeight));
1943
+ svg.innerHTML='';
1944
+ // Lane separators + labels
1945
+ categoriesManual.forEach(cat=>{
1946
+ const laneY = vPad + (catIndex.get(cat)||0)*laneHeight;
1947
+ const yMid = laneY + nodeHeight/2;
1948
+ const sep = document.createElementNS('http://www.w3.org/2000/svg','line');
1949
+ sep.setAttribute('x1','0'); sep.setAttribute('x2', String(totalWidth)); sep.setAttribute('y1', String(laneY + laneHeight - 8)); sep.setAttribute('y2', String(laneY + laneHeight - 8)); sep.setAttribute('stroke','#1f2e40'); sep.setAttribute('stroke-width','1'); sep.setAttribute('opacity','0.35'); svg.appendChild(sep);
1950
+ const text = document.createElementNS('http://www.w3.org/2000/svg','text');
1951
+ text.setAttribute('x','10'); text.setAttribute('y', String(yMid)); text.setAttribute('fill','#9fb5cc'); text.setAttribute('font-weight','600'); text.textContent = cat; svg.appendChild(text);
1952
+ });
1953
+ const edgeGroup = document.createElementNS('http://www.w3.org/2000/svg','g');
1954
+ edgeGroup.setAttribute('stroke','#5479ff'); edgeGroup.setAttribute('stroke-width','1.2'); edgeGroup.setAttribute('fill','none'); edgeGroup.setAttribute('opacity','0.9');
1955
+ const defs = document.createElementNS('http://www.w3.org/2000/svg','defs');
1956
+ const marker = document.createElementNS('http://www.w3.org/2000/svg','marker');
1957
+ marker.setAttribute('id','drill-arrow'); marker.setAttribute('markerWidth','10'); marker.setAttribute('markerHeight','6'); marker.setAttribute('refX','10'); marker.setAttribute('refY','3'); marker.setAttribute('orient','auto');
1958
+ const mpath = document.createElementNS('http://www.w3.org/2000/svg','path'); mpath.setAttribute('d','M0,0 L10,3 L0,6 Z'); mpath.setAttribute('fill','#5479ff'); marker.appendChild(mpath); defs.appendChild(marker); svg.appendChild(defs);
1959
+ edgeGroup.setAttribute('marker-end','url(#drill-arrow)');
1960
+ j.edges.filter(e=> positions.has(e.from) && positions.has(e.to)).forEach(e=>{
1961
+ const a = positions.get(e.from); const b = positions.get(e.to); if(!a||!b) return;
1962
+ const path = document.createElementNS('http://www.w3.org/2000/svg','path');
1963
+ const dx = (b.x - a.x)/2;
1964
+ const d = `M ${a.x + nodeWidth} ${a.y + nodeHeight/2} C ${a.x + nodeWidth + dx} ${a.y + nodeHeight/2}, ${b.x - dx} ${b.y + nodeHeight/2}, ${b.x} ${b.y + nodeHeight/2}`;
1965
+ path.setAttribute('d', d); path.setAttribute('stroke', e.type==='category'? '#8fb2ff':'#5479ff'); edgeGroup.appendChild(path);
1966
+ instrEdgeCount++;
1967
+ });
1968
+ svg.appendChild(edgeGroup);
1969
+ if(membershipEnabled){
1970
+ const membershipGroup = document.createElementNS('http://www.w3.org/2000/svg','g');
1971
+ membershipGroup.setAttribute('stroke','#44566d'); membershipGroup.setAttribute('stroke-width','1'); membershipGroup.setAttribute('opacity','0.8');
1972
+ membershipGroup.setAttribute('data-role','membership-group');
1973
+ instructions.forEach(inst=>{
1974
+ const pos = positions.get(inst.id); if(!pos) return;
1975
+ const yMid = pos.y + nodeHeight/2;
1976
+ const conn = document.createElementNS('http://www.w3.org/2000/svg','path');
1977
+ conn.setAttribute('d', `M 150 ${yMid} L ${pos.x} ${yMid}`);
1978
+ membershipGroup.appendChild(conn);
1979
+ });
1980
+ svg.appendChild(membershipGroup);
1981
+ }
1982
+ instructions.forEach(inst=>{
1983
+ const pos = positions.get(inst.id); if(!pos) return;
1984
+ const g = document.createElementNS('http://www.w3.org/2000/svg','g'); g.setAttribute('transform', `translate(${pos.x},${pos.y})`); g.setAttribute('data-id', inst.id); g.style.cursor='pointer';
1985
+ g.setAttribute('data-cat', (inst.categories&&inst.categories[0])||'uncategorized');
1986
+ const rect = document.createElementNS('http://www.w3.org/2000/svg','rect'); rect.setAttribute('width', String(nodeWidth)); rect.setAttribute('height', String(nodeHeight)); rect.setAttribute('rx','6'); rect.setAttribute('fill','#3a4554'); rect.setAttribute('stroke','#6b8cff'); rect.classList.add('drill-node-rect');
1987
+ const label = document.createElementNS('http://www.w3.org/2000/svg','text'); label.setAttribute('x','8'); label.setAttribute('y','20'); label.setAttribute('fill','#e3ebf5'); label.textContent = inst.id; label.setAttribute('pointer-events','none');
1988
+ const title = document.createElementNS('http://www.w3.org/2000/svg','title'); title.textContent = inst.id; g.appendChild(title);
1989
+ g.appendChild(rect); g.appendChild(label); svg.appendChild(g);
1990
+ g.addEventListener('mouseenter', ()=> rect.setAttribute('stroke','#8fb2ff'));
1991
+ g.addEventListener('mouseleave', ()=>{ if(!g.classList.contains('selected')) rect.setAttribute('stroke','#6b8cff'); });
1992
+ g.addEventListener('click', ()=> selectDrillNode(inst.id));
1993
+ });
1994
+ if(status) status.textContent = `rendered ${instructions.length} nodes across ${categoriesManual.length} lane(s)`;
1995
+ }
1996
+ // Legend update
1997
+ updateDrillLegend({
1998
+ selected: ids.length,
1999
+ expanded: j.expanded||0,
2000
+ total: instructions.length,
2001
+ edges: instrEdgeCount,
2002
+ membership: membershipEnabled? instructions.length:0,
2003
+ layout: usedElk? 'ELK':'Manual',
2004
+ expansionEnabled: expandEnabled
2005
+ });
2006
+ } catch(e){ if(status) status.textContent='render error: ' + (e?.message||e); }
2007
+ }
2008
+ function updateDrillLegend(data){
2009
+ const el = document.getElementById('drill-legend'); if(!el) return;
2010
+ el.innerHTML = `<div class="drill-legend-title">Legend / Stats</div>` +
2011
+ `<div class="drill-legend-body">`+
2012
+ `Selected: <b>${data.selected}</b>${data.expansionEnabled? ' (expand '+(data.expanded||0)+')':''}<br>`+
2013
+ `Rendered Instructions: <b>${data.total}</b><br>`+
2014
+ `Instruction Edges: <b>${data.edges}</b><br>`+
2015
+ `Membership Lines: <b>${data.membership}</b> ${data.membership? '(primary category connectors)':''}<br>`+
2016
+ `Layout: <b>${data.layout}</b>${data.expansionEnabled? ' + expand':''}`+
2017
+ `</div>`;
2018
+ }
2019
+ function exportDrillSvg(){
2020
+ const svg = document.getElementById('drill-svg'); if(!svg || !svg.firstChild) return;
2021
+ const clone = svg.cloneNode(true);
2022
+ if(clone instanceof SVGElement){
2023
+ clone.setAttribute('xmlns','http://www.w3.org/2000/svg');
2024
+ }
2025
+ const ser = new XMLSerializer();
2026
+ const data = ser.serializeToString(clone);
2027
+ const blob = new Blob([data], { type:'image/svg+xml;charset=utf-8' });
2028
+ const url = URL.createObjectURL(blob);
2029
+ const a = document.createElement('a');
2030
+ const ts = new Date().toISOString().replace(/[:T]/g,'-').slice(0,19);
2031
+ a.href = url; a.download = 'drilldown-graph-'+ts+'.svg';
2032
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
2033
+ setTimeout(()=> URL.revokeObjectURL(url), 2500);
2034
+ }
2035
+ function applyLaneFilter(){
2036
+ const active = window.__drillLaneFilter || null;
2037
+ const svg = document.getElementById('drill-svg'); if(!svg) return;
2038
+ const nodes = svg.querySelectorAll('g[data-id]');
2039
+ nodes.forEach(n=>{
2040
+ const cat = n.getAttribute('data-cat');
2041
+ if(!active || cat===active){
2042
+ n.style.opacity='1';
2043
+ } else {
2044
+ n.style.opacity='0.15';
2045
+ }
2046
+ });
2047
+ // Dull membership lines when filter active
2048
+ const membershipGroup = svg.querySelector('[data-role="membership-group"]');
2049
+ if(membershipGroup){ membershipGroup.setAttribute('opacity', active? '0.25':'0.8'); }
2050
+ // Highlight anchors
2051
+ svg.querySelectorAll('[data-cat-anchor]').forEach(a=>{
2052
+ if(a.getAttribute('data-cat-anchor')===active){ a.setAttribute('fill','#6b8cff'); a.setAttribute('stroke','#ffd36b'); }
2053
+ else { a.setAttribute('fill','#2e3d50'); a.setAttribute('stroke','#6b8cff'); }
2054
+ });
2055
+ }
2056
+ function toggleLaneFilter(cat){
2057
+ if(window.__drillLaneFilter === cat){ window.__drillLaneFilter = null; }
2058
+ else { window.__drillLaneFilter = cat; }
2059
+ applyLaneFilter();
2060
+ }
2061
+ // Control wiring (idempotent safeguard)
2062
+ (function(){
2063
+ const exp = document.getElementById('drill-expand'); if(exp && !exp.__wired){ exp.addEventListener('change', ()=> renderDrillSvg()); exp.__wired=true; }
2064
+ const mem = document.getElementById('drill-show-membership'); if(mem && !mem.__wired){ mem.addEventListener('change', ()=> renderDrillSvg()); mem.__wired=true; }
2065
+ const exb = document.getElementById('drill-export'); if(exb && !exb.__wired){ exb.addEventListener('click', exportDrillSvg); exb.__wired=true; }
2066
+ const hed = document.getElementById('mermaid-high-edges');
2067
+ if(hed && !hed.__wired){
2068
+ hed.addEventListener('change', async ()=>{
2069
+ // Set override variable then force reload mermaid core
2070
+ if(hed.checked){ window.__MERMAID_MAX_EDGES = 10000; } else { window.__MERMAID_MAX_EDGES = 3000; }
2071
+ await ensureMermaid(true);
2072
+ // Re-render existing mermaid graph if source present
2073
+ try {
2074
+ const srcEl = document.getElementById('graph-mermaid');
2075
+ const host = document.getElementById('graph-mermaid-svg');
2076
+ if(srcEl && host && window.mermaid){
2077
+ const codeRaw = srcEl.textContent || '';
2078
+ const code = ensureMermaidDirective(codeRaw);
2079
+ host.innerHTML = `<div class="mermaid">${code}</div>`;
2080
+ await window.mermaid.run({ querySelector:'#graph-mermaid-svg .mermaid' });
2081
+ if(window.__GRAPH_DEBUG || document.getElementById('graph-debug')?.checked){
2082
+ graphLog('re-render after high edge cap toggle', { firstLine: code.split(/\n/)[0] });
2083
+ }
2084
+ }
2085
+ const meta2 = document.getElementById('graph-meta2');
2086
+ if(meta2){ meta2.textContent = (meta2.textContent||'') + ` | maxEdges=${window.__MERMAID_ACTIVE_MAX_EDGES}`; }
2087
+ } catch(e){ console.warn('Mermaid re-render after high-edge toggle failed', e); }
2088
+ });
2089
+ hed.__wired = true;
2090
+ }
2091
+ const lg = document.getElementById('mermaid-large-graph');
2092
+ if(lg && !lg.__wired){
2093
+ lg.addEventListener('change', async ()=>{
2094
+ window.__MERMAID_LARGE_GRAPH_FLAG = !!lg.checked;
2095
+ // Clear explicit override so flag logic chooses caps unless high-edge override also set.
2096
+ if(!window.__MERMAID_LARGE_GRAPH_FLAG && typeof window.__MERMAID_MAX_EDGES === 'number'){
2097
+ // keep existing manual override; do nothing
2098
+ }
2099
+ await ensureMermaid(true);
2100
+ try {
2101
+ const srcEl = document.getElementById('graph-mermaid');
2102
+ const host = document.getElementById('graph-mermaid-svg');
2103
+ if(srcEl && host && window.mermaid){
2104
+ const codeRaw = srcEl.textContent || '';
2105
+ const code = ensureMermaidDirective(codeRaw);
2106
+ host.innerHTML = `<div class=\"mermaid\">${code}</div>`;
2107
+ await window.mermaid.run({ querySelector:'#graph-mermaid-svg .mermaid' });
2108
+ if(window.__GRAPH_DEBUG || document.getElementById('graph-debug')?.checked){
2109
+ graphLog('re-render after large graph toggle', { firstLine: code.split(/\n/)[0], largeGraph: window.__MERMAID_LARGE_GRAPH_FLAG });
2110
+ }
2111
+ }
2112
+ const meta2 = document.getElementById('graph-meta2');
2113
+ if(meta2){ meta2.textContent = (meta2.textContent||'') + ` | largeGraph=${window.__MERMAID_LARGE_GRAPH_FLAG} maxEdges=${window.__MERMAID_ACTIVE_MAX_EDGES} maxTextSize=${window.__MERMAID_ACTIVE_MAX_TEXT_SIZE}`; }
2114
+ } catch(e){ console.warn('Mermaid re-render after large-graph toggle failed', e); }
2115
+ });
2116
+ lg.__wired = true;
2117
+ }
2118
+ })();
2119
+ function selectDrillNode(id){
2120
+ const svg = document.getElementById('drill-svg'); if(!svg) return;
2121
+ // Clear previous selection
2122
+ [...svg.querySelectorAll('g.selected')].forEach(g=>{ g.classList.remove('selected'); const rect = g.querySelector('rect.drill-node-rect'); if(rect) rect.setAttribute('stroke','#6b8cff'); });
2123
+ const target = svg.querySelector(`g[data-id="${CSS.escape(id)}"]`);
2124
+ if(target){
2125
+ target.classList.add('selected'); const rect = target.querySelector('rect.drill-node-rect'); if(rect) rect.setAttribute('stroke','#ffd36b');
2126
+ }
2127
+ const data = window.__lastDrillData; const detail = document.getElementById('drill-detail');
2128
+ if(!data || !detail) return;
2129
+ const inst = data.instructions.find(i=> i.id===id); if(!inst){ detail.style.display='none'; return; }
2130
+ // Compute incident edges
2131
+ const edges = data.raw.edges.filter(e=> e.from===id || e.to===id);
2132
+ const incoming = edges.filter(e=> e.to===id).map(e=> e.from);
2133
+ const outgoing = edges.filter(e=> e.from===id).map(e=> e.to);
2134
+ const cats = inst.categories || [];
2135
+ detail.style.display='block';
2136
+ detail.innerHTML = `<div class="drill-detail-heading">📄 ${id}</div>` +
2137
+ `<div class="drill-detail-sub">Primary: ${(inst.primaryCategory||cats[0]||'–')}` +
2138
+ (cats.length? ` | Categories: ${cats.join(', ')}`:'') + `</div>` +
2139
+ `<div class="drill-detail-section">Incoming (${incoming.length}): ${incoming.length? incoming.join(', '):'none'}</div>` +
2140
+ `<div class="drill-detail-section">Outgoing (${outgoing.length}): ${outgoing.length? outgoing.join(', '):'none'}</div>` +
2141
+ `<div class="drill-detail-footer">Click another node to update. (Future: open file / copy / filter.)</div>`;
2142
+ }
2143
+ // Auto-preload categories when graph section first opened (best-effort)
2144
+ document.addEventListener('visibilitychange', ()=>{
2145
+ if(!document.hidden){ const catSel = document.getElementById('drill-categories'); if(catSel && !catSel.options.length) refreshDrillCategories(); }
2146
+ });
2147
+ // ----------------------------------------------------------------------
2148
+ </script>
2149
+ </body>
2150
+ </html>