@dollhousemcp/mcp-server 2.0.0-rc.6 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/CHANGELOG.md +52 -61
  2. package/README.github.md +2 -2
  3. package/README.md +2 -2
  4. package/README.md.backup +284 -224
  5. package/README.npm.md +2 -2
  6. package/dist/cache/LRUCache.d.ts +3 -0
  7. package/dist/cache/LRUCache.d.ts.map +1 -1
  8. package/dist/cache/LRUCache.js +36 -26
  9. package/dist/config/env.d.ts +14 -4
  10. package/dist/config/env.d.ts.map +1 -1
  11. package/dist/config/env.js +20 -6
  12. package/dist/di/Container.d.ts +21 -0
  13. package/dist/di/Container.d.ts.map +1 -1
  14. package/dist/di/Container.js +250 -53
  15. package/dist/elements/BaseElement.d.ts.map +1 -1
  16. package/dist/elements/BaseElement.js +5 -10
  17. package/dist/elements/base/BaseElementManager.d.ts +22 -0
  18. package/dist/elements/base/BaseElementManager.d.ts.map +1 -1
  19. package/dist/elements/base/BaseElementManager.js +47 -7
  20. package/dist/elements/memories/Memory.d.ts +1 -0
  21. package/dist/elements/memories/Memory.d.ts.map +1 -1
  22. package/dist/elements/memories/Memory.js +12 -8
  23. package/dist/elements/memories/MemoryManager.d.ts.map +1 -1
  24. package/dist/elements/memories/MemoryManager.js +23 -42
  25. package/dist/elements/memories/MemorySearchIndex.js +2 -2
  26. package/dist/generated/version.d.ts +2 -2
  27. package/dist/generated/version.d.ts.map +1 -1
  28. package/dist/generated/version.js +3 -3
  29. package/dist/handlers/EnhancedIndexHandler.js +6 -6
  30. package/dist/handlers/element-crud/listElements.d.ts +2 -0
  31. package/dist/handlers/element-crud/listElements.d.ts.map +1 -1
  32. package/dist/handlers/element-crud/listElements.js +3 -1
  33. package/dist/handlers/mcp-aql/Gatekeeper.d.ts.map +1 -1
  34. package/dist/handlers/mcp-aql/Gatekeeper.js +23 -17
  35. package/dist/handlers/mcp-aql/MCPAQLHandler.d.ts +14 -0
  36. package/dist/handlers/mcp-aql/MCPAQLHandler.d.ts.map +1 -1
  37. package/dist/handlers/mcp-aql/MCPAQLHandler.js +110 -14
  38. package/dist/handlers/mcp-aql/OperationRouter.d.ts.map +1 -1
  39. package/dist/handlers/mcp-aql/OperationRouter.js +13 -1
  40. package/dist/handlers/mcp-aql/OperationSchema.d.ts +7 -0
  41. package/dist/handlers/mcp-aql/OperationSchema.d.ts.map +1 -1
  42. package/dist/handlers/mcp-aql/OperationSchema.js +52 -1
  43. package/dist/handlers/mcp-aql/evaluatePermission.d.ts +53 -0
  44. package/dist/handlers/mcp-aql/evaluatePermission.d.ts.map +1 -0
  45. package/dist/handlers/mcp-aql/evaluatePermission.js +132 -0
  46. package/dist/handlers/mcp-aql/policies/ToolClassification.d.ts.map +1 -1
  47. package/dist/handlers/mcp-aql/policies/ToolClassification.js +2 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +3 -3
  50. package/dist/logging/LogHooks.js +11 -11
  51. package/dist/logging/LogManager.d.ts +0 -2
  52. package/dist/logging/LogManager.d.ts.map +1 -1
  53. package/dist/logging/LogManager.js +1 -3
  54. package/dist/logging/sinks/MemoryLogSink.d.ts +2 -0
  55. package/dist/logging/sinks/MemoryLogSink.d.ts.map +1 -1
  56. package/dist/logging/sinks/MemoryLogSink.js +12 -3
  57. package/dist/logging/types.d.ts +0 -2
  58. package/dist/logging/types.d.ts.map +1 -1
  59. package/dist/logging/types.js +1 -1
  60. package/dist/metrics/GatekeeperMetricsTracker.d.ts +32 -0
  61. package/dist/metrics/GatekeeperMetricsTracker.d.ts.map +1 -0
  62. package/dist/metrics/GatekeeperMetricsTracker.js +42 -0
  63. package/dist/metrics/MetricsManager.d.ts +47 -0
  64. package/dist/metrics/MetricsManager.d.ts.map +1 -0
  65. package/dist/metrics/MetricsManager.js +232 -0
  66. package/dist/metrics/OperationMetricsTracker.d.ts +32 -0
  67. package/dist/metrics/OperationMetricsTracker.d.ts.map +1 -0
  68. package/dist/metrics/OperationMetricsTracker.js +53 -0
  69. package/dist/metrics/collectors/DefaultElementProviderCollector.d.ts +27 -0
  70. package/dist/metrics/collectors/DefaultElementProviderCollector.d.ts.map +1 -0
  71. package/dist/metrics/collectors/DefaultElementProviderCollector.js +69 -0
  72. package/dist/metrics/collectors/FileLockManagerCollector.d.ts +16 -0
  73. package/dist/metrics/collectors/FileLockManagerCollector.d.ts.map +1 -0
  74. package/dist/metrics/collectors/FileLockManagerCollector.js +51 -0
  75. package/dist/metrics/collectors/GatekeeperMetricsCollector.d.ts +16 -0
  76. package/dist/metrics/collectors/GatekeeperMetricsCollector.d.ts.map +1 -0
  77. package/dist/metrics/collectors/GatekeeperMetricsCollector.js +76 -0
  78. package/dist/metrics/collectors/LRUCacheCollector.d.ts +18 -0
  79. package/dist/metrics/collectors/LRUCacheCollector.d.ts.map +1 -0
  80. package/dist/metrics/collectors/LRUCacheCollector.js +95 -0
  81. package/dist/metrics/collectors/OperationMetricsCollector.d.ts +16 -0
  82. package/dist/metrics/collectors/OperationMetricsCollector.d.ts.map +1 -0
  83. package/dist/metrics/collectors/OperationMetricsCollector.js +80 -0
  84. package/dist/metrics/collectors/OperationalTelemetryCollector.d.ts +17 -0
  85. package/dist/metrics/collectors/OperationalTelemetryCollector.d.ts.map +1 -0
  86. package/dist/metrics/collectors/OperationalTelemetryCollector.js +26 -0
  87. package/dist/metrics/collectors/PerformanceMonitorCollector.d.ts +14 -0
  88. package/dist/metrics/collectors/PerformanceMonitorCollector.d.ts.map +1 -0
  89. package/dist/metrics/collectors/PerformanceMonitorCollector.js +141 -0
  90. package/dist/metrics/collectors/SecurityMonitorCollector.d.ts +21 -0
  91. package/dist/metrics/collectors/SecurityMonitorCollector.d.ts.map +1 -0
  92. package/dist/metrics/collectors/SecurityMonitorCollector.js +56 -0
  93. package/dist/metrics/collectors/SecurityTelemetryCollector.d.ts +15 -0
  94. package/dist/metrics/collectors/SecurityTelemetryCollector.d.ts.map +1 -0
  95. package/dist/metrics/collectors/SecurityTelemetryCollector.js +112 -0
  96. package/dist/metrics/collectors/TriggerMetricsTrackerCollector.d.ts +16 -0
  97. package/dist/metrics/collectors/TriggerMetricsTrackerCollector.d.ts.map +1 -0
  98. package/dist/metrics/collectors/TriggerMetricsTrackerCollector.js +26 -0
  99. package/dist/metrics/collectors/index.d.ts +11 -0
  100. package/dist/metrics/collectors/index.d.ts.map +1 -0
  101. package/dist/metrics/collectors/index.js +11 -0
  102. package/dist/metrics/sinks/MemoryMetricsSink.d.ts +22 -0
  103. package/dist/metrics/sinks/MemoryMetricsSink.d.ts.map +1 -0
  104. package/dist/metrics/sinks/MemoryMetricsSink.js +121 -0
  105. package/dist/metrics/types.d.ts +98 -0
  106. package/dist/metrics/types.d.ts.map +1 -0
  107. package/dist/metrics/types.js +24 -0
  108. package/dist/portfolio/DefaultElementProvider.d.ts.map +1 -1
  109. package/dist/portfolio/DefaultElementProvider.js +1 -7
  110. package/dist/portfolio/EnhancedIndexManager.d.ts.map +1 -1
  111. package/dist/portfolio/EnhancedIndexManager.js +18 -18
  112. package/dist/portfolio/NLPScoringManager.d.ts.map +1 -1
  113. package/dist/portfolio/NLPScoringManager.js +5 -9
  114. package/dist/portfolio/PortfolioIndexManager.js +2 -2
  115. package/dist/portfolio/RelationshipManager.js +2 -2
  116. package/dist/portfolio/VerbTriggerManager.d.ts.map +1 -1
  117. package/dist/portfolio/VerbTriggerManager.js +5 -19
  118. package/dist/portfolio/config/IndexConfig.d.ts.map +1 -1
  119. package/dist/portfolio/config/IndexConfig.js +1 -12
  120. package/dist/portfolio/enhanced-index/ElementDefinitionBuilder.d.ts.map +1 -1
  121. package/dist/portfolio/enhanced-index/ElementDefinitionBuilder.js +3 -15
  122. package/dist/portfolio/enhanced-index/SemanticRelationshipService.d.ts.map +1 -1
  123. package/dist/portfolio/enhanced-index/SemanticRelationshipService.js +2 -16
  124. package/dist/portfolio/types/RelationshipTypes.d.ts.map +1 -1
  125. package/dist/portfolio/types/RelationshipTypes.js +3 -17
  126. package/dist/security/audit/config/suppressions.d.ts.map +1 -1
  127. package/dist/security/audit/config/suppressions.js +36 -8
  128. package/dist/security/constants.d.ts.map +1 -1
  129. package/dist/security/constants.js +10 -6
  130. package/dist/security/fileLockManager.d.ts.map +1 -1
  131. package/dist/security/fileLockManager.js +8 -6
  132. package/dist/security/secureYamlParser.d.ts.map +1 -1
  133. package/dist/security/secureYamlParser.js +1 -13
  134. package/dist/security/securityMonitor.d.ts +2 -1
  135. package/dist/security/securityMonitor.d.ts.map +1 -1
  136. package/dist/security/securityMonitor.js +14 -3
  137. package/dist/security/telemetry/SecurityTelemetry.d.ts +16 -0
  138. package/dist/security/telemetry/SecurityTelemetry.d.ts.map +1 -1
  139. package/dist/security/telemetry/SecurityTelemetry.js +30 -2
  140. package/dist/security/tokenManager.d.ts +3 -0
  141. package/dist/security/tokenManager.d.ts.map +1 -1
  142. package/dist/security/tokenManager.js +13 -5
  143. package/dist/security/validation/BackgroundValidator.d.ts.map +1 -1
  144. package/dist/security/validation/BackgroundValidator.js +7 -7
  145. package/dist/server/startup.d.ts.map +1 -1
  146. package/dist/server/startup.js +8 -24
  147. package/dist/server/tools/MCPAQLTools.js +4 -1
  148. package/dist/services/ActivationStore.d.ts.map +1 -1
  149. package/dist/services/ActivationStore.js +9 -3
  150. package/dist/services/FileWatchService.d.ts +1 -0
  151. package/dist/services/FileWatchService.d.ts.map +1 -1
  152. package/dist/services/FileWatchService.js +83 -48
  153. package/dist/services/MetadataService.d.ts.map +1 -1
  154. package/dist/services/MetadataService.js +7 -2
  155. package/dist/services/query/ElementQueryService.d.ts.map +1 -1
  156. package/dist/services/query/ElementQueryService.js +1 -41
  157. package/dist/services/query/PaginationService.d.ts.map +1 -1
  158. package/dist/services/query/PaginationService.js +1 -14
  159. package/dist/services/query/SortService.d.ts.map +1 -1
  160. package/dist/services/query/SortService.js +1 -6
  161. package/dist/services/validation/ValidationService.d.ts.map +1 -1
  162. package/dist/services/validation/ValidationService.js +3 -8
  163. package/dist/storage/ElementStorageLayer.d.ts.map +1 -1
  164. package/dist/storage/ElementStorageLayer.js +5 -2
  165. package/dist/storage/MemoryStorageLayer.d.ts.map +1 -1
  166. package/dist/storage/MemoryStorageLayer.js +5 -2
  167. package/dist/telemetry/OperationalTelemetry.js +2 -2
  168. package/dist/utils/EventDeduplicator.d.ts +44 -0
  169. package/dist/utils/EventDeduplicator.d.ts.map +1 -0
  170. package/dist/utils/EventDeduplicator.js +93 -0
  171. package/dist/utils/FileLock.d.ts.map +1 -1
  172. package/dist/utils/FileLock.js +1 -9
  173. package/dist/utils/PerformanceMonitor.d.ts.map +1 -1
  174. package/dist/utils/PerformanceMonitor.js +5 -5
  175. package/dist/utils/SlidingWindowRateLimiter.d.ts +13 -0
  176. package/dist/utils/SlidingWindowRateLimiter.d.ts.map +1 -0
  177. package/dist/utils/SlidingWindowRateLimiter.js +23 -0
  178. package/dist/web/console/IngestRoutes.d.ts +84 -0
  179. package/dist/web/console/IngestRoutes.d.ts.map +1 -0
  180. package/dist/web/console/IngestRoutes.js +252 -0
  181. package/dist/web/console/LeaderElection.d.ts +89 -0
  182. package/dist/web/console/LeaderElection.d.ts.map +1 -0
  183. package/dist/web/console/LeaderElection.js +205 -0
  184. package/dist/web/console/LeaderForwardingSink.d.ts +61 -0
  185. package/dist/web/console/LeaderForwardingSink.d.ts.map +1 -0
  186. package/dist/web/console/LeaderForwardingSink.js +197 -0
  187. package/dist/web/console/SessionNames.d.ts +46 -0
  188. package/dist/web/console/SessionNames.d.ts.map +1 -0
  189. package/dist/web/console/SessionNames.js +257 -0
  190. package/dist/web/console/UnifiedConsole.d.ts +64 -0
  191. package/dist/web/console/UnifiedConsole.d.ts.map +1 -0
  192. package/dist/web/console/UnifiedConsole.js +119 -0
  193. package/dist/web/contentPipeline.d.ts +58 -0
  194. package/dist/web/contentPipeline.d.ts.map +1 -0
  195. package/dist/web/contentPipeline.js +112 -0
  196. package/dist/web/portDiscovery.d.ts +58 -0
  197. package/dist/web/portDiscovery.d.ts.map +1 -0
  198. package/dist/web/portDiscovery.js +143 -0
  199. package/dist/web/public/app.js +148 -60
  200. package/dist/web/public/logs.js +638 -0
  201. package/dist/web/public/metrics.js +682 -0
  202. package/dist/web/public/permissions.js +394 -0
  203. package/dist/web/public/sessions.js +369 -0
  204. package/dist/web/routes/healthRoutes.d.ts +16 -0
  205. package/dist/web/routes/healthRoutes.d.ts.map +1 -0
  206. package/dist/web/routes/healthRoutes.js +29 -0
  207. package/dist/web/routes/logRoutes.d.ts +18 -0
  208. package/dist/web/routes/logRoutes.d.ts.map +1 -0
  209. package/dist/web/routes/logRoutes.js +126 -0
  210. package/dist/web/routes/metricsRoutes.d.ts +17 -0
  211. package/dist/web/routes/metricsRoutes.d.ts.map +1 -0
  212. package/dist/web/routes/metricsRoutes.js +90 -0
  213. package/dist/web/routes/permissionRoutes.d.ts +16 -0
  214. package/dist/web/routes/permissionRoutes.d.ts.map +1 -0
  215. package/dist/web/routes/permissionRoutes.js +133 -0
  216. package/dist/web/routes.d.ts.map +1 -1
  217. package/dist/web/routes.js +309 -339
  218. package/dist/web/server.d.ts +21 -1
  219. package/dist/web/server.d.ts.map +1 -1
  220. package/dist/web/server.js +42 -4
  221. package/dist/web/sinks/WebSSELogSink.d.ts +15 -0
  222. package/dist/web/sinks/WebSSELogSink.d.ts.map +1 -0
  223. package/dist/web/sinks/WebSSELogSink.js +22 -0
  224. package/dist/web/sinks/WebSSEMetricsSink.d.ts +16 -0
  225. package/dist/web/sinks/WebSSEMetricsSink.d.ts.map +1 -0
  226. package/dist/web/sinks/WebSSEMetricsSink.js +23 -0
  227. package/package.json +2 -2
  228. package/server.json +2 -2
  229. package/dist/constants/version.d.ts +0 -3
  230. package/dist/constants/version.d.ts.map +0 -1
  231. package/dist/constants/version.js +0 -4
  232. package/dist/logging/sinks/SSELogSink.d.ts +0 -35
  233. package/dist/logging/sinks/SSELogSink.d.ts.map +0 -1
  234. package/dist/logging/sinks/SSELogSink.js +0 -181
  235. package/dist/logging/viewer/viewerHtml.d.ts +0 -8
  236. package/dist/logging/viewer/viewerHtml.d.ts.map +0 -1
  237. package/dist/logging/viewer/viewerHtml.js +0 -204
  238. package/dist/web/public/public/app.js +0 -1878
@@ -0,0 +1,682 @@
1
+ /**
2
+ * DollhouseMCP Metrics Dashboard
3
+ *
4
+ * Displays system health, cache efficiency, security, and more.
5
+ * Fetches data via polling (GET /api/metrics).
6
+ * Uses uPlot for time-series charts when available.
7
+ */
8
+
9
+ (() => {
10
+ const POLL_INTERVAL_MS = 15000;
11
+ const TIME_RANGES = {
12
+ '15m': 15 * 60 * 1000,
13
+ '30m': 30 * 60 * 1000,
14
+ '1h': 60 * 60 * 1000,
15
+ };
16
+
17
+ // ── State ────────────────────────────────────────────────────────────────
18
+ let pollTimer = null;
19
+ let activeRange = '15m';
20
+ let lastSnapshot = null;
21
+ let historySnapshots = []; // for time-series charts
22
+ let charts = {}; // uPlot instances by section
23
+ let uPlotAvailable = false;
24
+
25
+ // ── Public API ───────────────────────────────────────────────────────────
26
+ globalThis.DollhouseConsole = globalThis.DollhouseConsole || {};
27
+ globalThis.DollhouseConsole.metrics = {
28
+ init: initMetrics,
29
+ destroy: destroyMetrics,
30
+ refresh: () => {
31
+ if (lastSnapshot) {
32
+ requestAnimationFrame(() => renderAll(lastSnapshot.metrics));
33
+ }
34
+ },
35
+ };
36
+
37
+ function initMetrics() {
38
+ const container = document.getElementById('metrics-dashboard-root');
39
+ if (!container || container.dataset.initialized === 'true') return;
40
+ container.dataset.initialized = 'true';
41
+
42
+ uPlotAvailable = typeof globalThis.uPlot !== 'undefined'; // NOSONAR — typeof is the safe check for optional globals that may not be loaded
43
+ buildDOM(container);
44
+ bindEvents();
45
+ fetchLatest();
46
+ pollTimer = setInterval(fetchLatest, POLL_INTERVAL_MS);
47
+ }
48
+
49
+ function destroyMetrics() {
50
+ if (pollTimer) {
51
+ clearInterval(pollTimer);
52
+ pollTimer = null;
53
+ }
54
+ for (const chart of Object.values(charts)) {
55
+ if (chart?.destroy) chart.destroy();
56
+ }
57
+ charts = {};
58
+ }
59
+
60
+ // ── DOM construction ─────────────────────────────────────────────────────
61
+ function buildDOM(container) {
62
+ container.innerHTML = `
63
+ <div class="metrics-status-bar">
64
+ <span id="metrics-last-update">No data yet</span>
65
+ <span id="metrics-collection-info"></span>
66
+ <div class="metrics-time-range">
67
+ <button class="metrics-time-btn active" data-range="15m">15m</button>
68
+ <button class="metrics-time-btn" data-range="30m">30m</button>
69
+ <button class="metrics-time-btn" data-range="1h">1h</button>
70
+ </div>
71
+ </div>
72
+ <div class="metrics-dashboard" id="metrics-grid">
73
+ ${buildCard('system', 'System Health')}
74
+ ${buildCard('search', 'Search Performance')}
75
+ ${buildCard('operations', 'MCP-AQL Operations')}
76
+ ${buildCard('cache', 'Cache Efficiency')}
77
+ ${buildCard('security', 'Security')}
78
+ ${buildCard('gatekeeper', 'Gatekeeper Policy')}
79
+ ${buildCard('locks', 'Locks & I/O')}
80
+ ${buildCard('meta', 'Metrics System')}
81
+ </div>
82
+ `;
83
+ }
84
+
85
+ function buildCard(id, title) {
86
+ return `
87
+ <div class="metrics-card" id="metrics-card-${id}">
88
+ <div class="metrics-card-header" data-card="${id}">
89
+ <span class="metrics-card-title">${title}</span>
90
+ <span class="metrics-card-toggle">&#9660;</span>
91
+ </div>
92
+ <div class="metrics-card-body" id="metrics-body-${id}">
93
+ <div class="metrics-loading">Waiting for data...</div>
94
+ </div>
95
+ </div>
96
+ `;
97
+ }
98
+
99
+ function bindEvents() {
100
+ document.getElementById('metrics-grid').addEventListener('click', (e) => {
101
+ const header = e.target.closest('.metrics-card-header');
102
+ if (!header) return;
103
+ header.parentElement.classList.toggle('collapsed');
104
+ });
105
+
106
+ document.querySelector('.metrics-time-range').addEventListener('click', (e) => {
107
+ const btn = e.target.closest('.metrics-time-btn');
108
+ if (!btn) return;
109
+ document.querySelectorAll('.metrics-time-btn').forEach(b => b.classList.remove('active'));
110
+ btn.classList.add('active');
111
+ activeRange = btn.dataset.range;
112
+ fetchHistory();
113
+ });
114
+ }
115
+
116
+ // ── Data fetching ────────────────────────────────────────────────────────
117
+ async function fetchLatest() {
118
+ try {
119
+ const res = await fetch('/api/metrics?latest=true');
120
+ if (!res.ok) return;
121
+ const data = await res.json();
122
+ if (data.snapshots?.length > 0) {
123
+ lastSnapshot = data.snapshots[0];
124
+ // Deduplicate by snapshot id
125
+ if (!historySnapshots.some(s => s.id === lastSnapshot.id)) {
126
+ historySnapshots.push(lastSnapshot);
127
+ }
128
+ // Trim history
129
+ const cutoff = Date.now() - TIME_RANGES['1h'];
130
+ historySnapshots = historySnapshots.filter(s => new Date(s.timestamp).getTime() > cutoff);
131
+ renderAll(lastSnapshot.metrics);
132
+ }
133
+ } catch { /* network error, will retry */ }
134
+ }
135
+
136
+ async function fetchHistory() {
137
+ try {
138
+ const since = new Date(Date.now() - TIME_RANGES[activeRange]).toISOString();
139
+ const res = await fetch(`/api/metrics?latest=false&since=${since}&limit=100`);
140
+ if (!res.ok) return;
141
+ const data = await res.json();
142
+ if (data.snapshots) {
143
+ historySnapshots = data.snapshots.reverse(); // oldest first
144
+ if (lastSnapshot) renderAll(lastSnapshot.metrics);
145
+ }
146
+ } catch { /* network error */ }
147
+ }
148
+
149
+ // ── Rendering ────────────────────────────────────────────────────────────
150
+ function safeRender(fn, metrics) {
151
+ try { fn(metrics); } catch (err) { console.warn('[Metrics] Card render failed:', err); }
152
+ }
153
+
154
+ function renderAll(metrics) {
155
+ if (!metrics) return;
156
+
157
+ updateStatus();
158
+ safeRender(renderSystemHealth, metrics);
159
+ safeRender(renderSearchPerf, metrics);
160
+ safeRender(renderOperations, metrics);
161
+ safeRender(renderCacheEfficiency, metrics);
162
+ safeRender(renderSecurity, metrics);
163
+ safeRender(renderGatekeeper, metrics);
164
+ safeRender(renderLocks, metrics);
165
+ safeRender(renderMetaSystem, metrics);
166
+ }
167
+
168
+ function updateStatus() {
169
+ const el = document.getElementById('metrics-last-update');
170
+ const infoEl = document.getElementById('metrics-collection-info');
171
+ if (el && lastSnapshot) {
172
+ const d = new Date(lastSnapshot.timestamp);
173
+ el.textContent = 'Last update: ' + d.toLocaleTimeString();
174
+ }
175
+ if (infoEl && lastSnapshot) {
176
+ infoEl.textContent = lastSnapshot.metrics.length + ' metrics | ' + lastSnapshot.durationMs + 'ms collection';
177
+ }
178
+ }
179
+
180
+ // ── Section renderers ────────────────────────────────────────────────────
181
+
182
+ // Cache last-known-good system values so intermittent collector failures don't blank the card
183
+ let lastSystemVals = {};
184
+
185
+ function renderSystemHealth(metrics) {
186
+ const body = document.getElementById('metrics-body-system');
187
+ if (!body) return;
188
+
189
+ const heapUsed = findVal(metrics, 'system.memory.heap_used_bytes');
190
+ const rss = findVal(metrics, 'system.memory.rss_bytes');
191
+ const growthRate = findVal(metrics, 'system.memory.growth_rate');
192
+ const cpu = findVal(metrics, 'system.cpu.usage_seconds');
193
+ const uptime = findVal(metrics, 'system.uptime_seconds');
194
+
195
+ // Update cache with any non-null values
196
+ if (heapUsed != null) lastSystemVals.heapUsed = heapUsed;
197
+ if (rss != null) lastSystemVals.rss = rss;
198
+ if (growthRate != null) lastSystemVals.growthRate = growthRate;
199
+ if (cpu != null) lastSystemVals.cpu = cpu;
200
+ if (uptime != null) lastSystemVals.uptime = uptime;
201
+
202
+ const v = lastSystemVals;
203
+ const statsHtml = '<div class="metrics-stat-grid" id="system-stats">' +
204
+ statBox('Heap Used', formatBytes(v.heapUsed)) +
205
+ statBox('RSS', formatBytes(v.rss)) +
206
+ statBox('Growth', v.growthRate == null ? '-' : formatNumber(v.growthRate, 2) + ' MB/s') +
207
+ statBox('CPU', v.cpu == null ? '-' : formatNumber(v.cpu, 2) + ' s') +
208
+ statBox('Uptime', formatDuration(v.uptime)) +
209
+ '</div>';
210
+
211
+ const statsEl = body.querySelector('#system-stats');
212
+ if (statsEl) {
213
+ statsEl.outerHTML = statsHtml;
214
+ } else {
215
+ let html = statsHtml;
216
+ if (uPlotAvailable) {
217
+ html += '<div class="metrics-chart-container" id="chart-system"></div>';
218
+ }
219
+ body.innerHTML = html;
220
+ }
221
+
222
+ if (uPlotAvailable && historySnapshots.length >= 3 && v.heapUsed != null) {
223
+ updateChart('chart-system', 'system',
224
+ ['system.memory.heap_used_bytes', 'system.memory.rss_bytes'],
225
+ ['Heap', 'RSS'], formatBytes);
226
+ }
227
+ }
228
+
229
+ function renderSearchPerf(metrics) {
230
+ const body = document.getElementById('metrics-body-search');
231
+ if (!body) return;
232
+
233
+ const duration = findEntry(metrics, 'performance.search.duration');
234
+ const hitRate = findVal(metrics, 'performance.search.cache_hit_rate');
235
+ const slowCount = findVal(metrics, 'performance.search.slow_query_count');
236
+
237
+ const hasSearchMetrics = duration != null || hitRate != null || slowCount != null;
238
+
239
+ if (!hasSearchMetrics) {
240
+ body.innerHTML = '<div class="metrics-loading">No search metrics available — PerformanceMonitor collector may not be active</div>';
241
+ return;
242
+ }
243
+
244
+ let html = '<div class="metrics-stat-grid">';
245
+ if (duration?.type === 'histogram') {
246
+ const v = duration.value;
247
+ html += statBox('Avg', fmtMs(v.avg));
248
+ html += statBox('P50', fmtMs(v.p50));
249
+ html += statBox('P95', fmtMs(v.p95));
250
+ html += statBox('P99', fmtMs(v.p99));
251
+ html += statBox('Count', formatNumber(v.count || 0));
252
+ }
253
+ html += statBox('Cache Hit', hitRate == null ? '-' : formatPercent(hitRate));
254
+ html += statBox('Slow Queries', slowCount == null ? '-' : formatNumber(slowCount));
255
+ html += '</div>';
256
+
257
+ body.innerHTML = html;
258
+ }
259
+
260
+ function buildLabeledTable(entries, labelKey, headerLabel) {
261
+ let html = '<table class="metrics-table"><thead><tr><th>' + escapeHtml(headerLabel) + '</th><th>Count</th></tr></thead><tbody>';
262
+ for (const m of entries) {
263
+ const label = m.labels?.[labelKey] || '?';
264
+ html += '<tr><td>' + escapeHtml(label) + '</td><td>' + formatNumber(m.value) + '</td></tr>';
265
+ }
266
+ return html + '</tbody></table>';
267
+ }
268
+
269
+ function renderOperations(metrics) {
270
+ const body = document.getElementById('metrics-body-operations');
271
+ if (!body) return;
272
+
273
+ const totalOps = findVal(metrics, 'mcpaql.operations_total');
274
+ const failedOps = findVal(metrics, 'mcpaql.operations_failed_total');
275
+ const duration = findEntry(metrics, 'mcpaql.duration');
276
+
277
+ if (totalOps == null && duration == null) {
278
+ body.innerHTML = '<div class="metrics-loading">No operation metrics yet</div>';
279
+ return;
280
+ }
281
+
282
+ const errorRate = totalOps > 0 ? (failedOps || 0) / totalOps : 0;
283
+
284
+ let html = '<div class="metrics-stat-grid">';
285
+ html += statBox('Total Ops', formatNumber(totalOps || 0));
286
+ html += statBox('Error Rate', formatPercent(errorRate));
287
+ if (duration?.type === 'histogram') {
288
+ html += statBox('Avg', fmtMs(duration.value.avg));
289
+ html += statBox('P95', fmtMs(duration.value.p95));
290
+ }
291
+ html += '</div>';
292
+
293
+ const endpointMetrics = metrics.filter(m => m.name === 'mcpaql.by_endpoint');
294
+ if (endpointMetrics.length > 0) html += buildLabeledTable(endpointMetrics, 'endpoint', 'Endpoint');
295
+
296
+ const opMetrics = metrics.filter(m => m.name === 'mcpaql.by_operation');
297
+ if (opMetrics.length > 0) {
298
+ const sorted = opMetrics.slice().sort((a, b) => (b.value || 0) - (a.value || 0));
299
+ html += buildLabeledTable(sorted, 'operation', 'Operation');
300
+ }
301
+
302
+ body.innerHTML = html;
303
+ }
304
+
305
+ function renderCacheEfficiency(metrics) {
306
+ const body = document.getElementById('metrics-body-cache');
307
+ if (!body) return;
308
+
309
+ const cacheMetrics = metrics.filter(m => m.name.startsWith('cache.lru.'));
310
+ if (cacheMetrics.length === 0) {
311
+ body.innerHTML = '<div class="metrics-loading">No cache metrics available</div>';
312
+ return;
313
+ }
314
+
315
+ // Group by labels.cache_name
316
+ const caches = new Map();
317
+ for (const m of cacheMetrics) {
318
+ const name = m.labels?.cache_name || m.labels?.cache || 'unknown';
319
+ if (!caches.has(name)) caches.set(name, {});
320
+ caches.get(name)[m.name.replace('cache.lru.', '')] = m.value;
321
+ }
322
+
323
+ let totalMemMB = 0;
324
+ let html = '<table class="metrics-table"><thead><tr>' +
325
+ '<th>Cache</th><th>Hit Rate</th><th>Hits</th><th>Misses</th><th>Size</th><th>Evictions</th><th>Memory</th>' +
326
+ '</tr></thead><tbody>';
327
+
328
+ for (const [name, vals] of caches) {
329
+ const memMB = vals.memory_used_megabytes || 0;
330
+ totalMemMB += memMB;
331
+ html += '<tr>' +
332
+ '<td>' + escapeHtml(name) + '</td>' +
333
+ '<td>' + (vals.hit_rate == null ? '-' : formatPercent(vals.hit_rate)) + '</td>' +
334
+ '<td>' + formatNumber(vals.hits_total || 0) + '</td>' +
335
+ '<td>' + formatNumber(vals.misses_total || 0) + '</td>' +
336
+ '<td>' + formatNumber(vals.size_current || 0) + '</td>' +
337
+ '<td>' + formatNumber(vals.evictions_total || 0) + '</td>' +
338
+ '<td>' + formatMB(memMB) + '</td>' +
339
+ '</tr>';
340
+ }
341
+
342
+ html += '</tbody><tfoot><tr class="metrics-table-total">' +
343
+ '<td colspan="6" style="text-align:right;font-weight:700">Total Cache Memory</td>' +
344
+ '<td style="font-weight:700">' + formatMB(totalMemMB) + '</td>' +
345
+ '</tr></tfoot></table>';
346
+ body.innerHTML = html;
347
+ }
348
+
349
+ // Security card: cache for recent events fetch
350
+ let securityEventsCache = null;
351
+ let securityEventsCacheTime = 0;
352
+ const SECURITY_CACHE_TTL = 30000;
353
+
354
+ function renderSecurity(metrics) {
355
+ const body = document.getElementById('metrics-body-security');
356
+ if (!body) return;
357
+
358
+ const blocked24h = findVal(metrics, 'security.telemetry.blocked_24h');
359
+ const attacksPerHour = findVal(metrics, 'security.telemetry.attacks_per_hour');
360
+
361
+ let html = '<div class="metrics-stat-grid">';
362
+ html += statBox('Blocked (24h)', blocked24h == null ? '0' : formatNumber(blocked24h));
363
+ html += statBox('Attacks/hour', attacksPerHour == null ? '0' : formatNumber(attacksPerHour, 1));
364
+ html += '</div>';
365
+
366
+ html += '<div id="security-recent-events"><div class="metrics-loading">Loading recent events...</div></div>';
367
+
368
+ body.innerHTML = html;
369
+
370
+ // Fetch recent security events (cached for 30s)
371
+ const now = Date.now();
372
+ if (securityEventsCache && (now - securityEventsCacheTime) < SECURITY_CACHE_TTL) {
373
+ renderSecurityEvents(securityEventsCache);
374
+ } else {
375
+ fetch('/api/logs?category=security&level=warn&limit=5')
376
+ .then(r => r.ok ? r.json() : null)
377
+ .then(data => {
378
+ if (data?.entries) {
379
+ securityEventsCache = data.entries;
380
+ securityEventsCacheTime = Date.now();
381
+ renderSecurityEvents(data.entries);
382
+ }
383
+ })
384
+ .catch(() => {
385
+ const el = document.getElementById('security-recent-events');
386
+ if (el) el.innerHTML = '';
387
+ });
388
+ }
389
+ }
390
+
391
+ function renderSecurityEvents(entries) {
392
+ const el = document.getElementById('security-recent-events');
393
+ if (!el) return;
394
+
395
+ if (!entries || entries.length === 0) {
396
+ el.innerHTML = '<div class="metrics-loading">No recent security events</div>';
397
+ return;
398
+ }
399
+
400
+ let html = '<table class="metrics-table"><thead><tr><th>Time</th><th>Source</th><th>Message</th></tr></thead><tbody>';
401
+ for (let i = 0; i < entries.length; i++) {
402
+ const entry = entries[i];
403
+ const ago = formatTimeAgo(entry.timestamp);
404
+ const source = escapeHtml(entry.source || '');
405
+ const fullMsg = escapeHtml(entry.message || '');
406
+ const truncated = fullMsg.length > 80 ? fullMsg.substring(0, 80) + '...' : fullMsg;
407
+ const needsExpand = fullMsg.length > 80;
408
+ html += '<tr><td>' + ago + '</td><td>' + source + '</td><td>';
409
+ if (needsExpand) {
410
+ html += '<span class="sec-msg-short" id="sec-msg-short-' + i + '" style="cursor:pointer" title="Click to expand">' + truncated + '</span>';
411
+ html += '<span class="sec-msg-full" id="sec-msg-full-' + i + '" style="display:none;white-space:pre-wrap;word-break:break-word;cursor:pointer" title="Click to collapse">' + fullMsg + '</span>';
412
+ } else {
413
+ html += truncated;
414
+ }
415
+ html += '</td></tr>';
416
+ }
417
+ html += '</tbody></table>';
418
+ el.innerHTML = html;
419
+
420
+ // Bind click handlers for expandable messages
421
+ for (let i = 0; i < entries.length; i++) {
422
+ const short = document.getElementById('sec-msg-short-' + i);
423
+ const full = document.getElementById('sec-msg-full-' + i);
424
+ if (short && full) {
425
+ short.addEventListener('click', () => { short.style.display = 'none'; full.style.display = 'inline'; });
426
+ full.addEventListener('click', () => { full.style.display = 'none'; short.style.display = 'inline'; });
427
+ }
428
+ }
429
+ }
430
+
431
+ function formatTimeAgo(timestamp) {
432
+ if (!timestamp) return '-';
433
+ const diff = Date.now() - new Date(timestamp).getTime();
434
+ if (diff < 60000) return Math.floor(diff / 1000) + 's ago';
435
+ if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
436
+ if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
437
+ return Math.floor(diff / 86400000) + 'd ago';
438
+ }
439
+
440
+ function buildShareTable(entries, labelKey, headerLabel, total) {
441
+ let html = '<table class="metrics-table"><thead><tr><th>' + escapeHtml(headerLabel) + '</th><th>Count</th><th>Share</th></tr></thead><tbody>';
442
+ for (const m of entries) {
443
+ const label = m.labels?.[labelKey] || '?';
444
+ const share = total > 0 ? formatPercent(m.value / total) : '-';
445
+ html += '<tr><td>' + escapeHtml(label) + '</td><td>' + formatNumber(m.value) + '</td><td>' + share + '</td></tr>';
446
+ }
447
+ return html + '</tbody></table>';
448
+ }
449
+
450
+ function renderGatekeeperStats(metrics, total, allowed, denied, confirmations) {
451
+ const allowRate = total > 0 ? allowed / total : 0;
452
+ let html = '<div class="metrics-stat-grid">';
453
+ html += statBox('Decisions', formatNumber(total));
454
+ html += statBox('Allowed', formatNumber(allowed || 0));
455
+ html += statBox('Denied', formatNumber(denied || 0));
456
+ html += statBox('Allow Rate', formatPercent(allowRate));
457
+ if (confirmations > 0) html += statBox('Confirmations', formatNumber(confirmations));
458
+ html += '</div>';
459
+
460
+ const sourceMetrics = metrics.filter(m => m.name === 'gatekeeper.by_policy_source');
461
+ if (sourceMetrics.length > 0) html += buildShareTable(sourceMetrics, 'policy_source', 'Policy Source', total);
462
+
463
+ const levelMetrics = metrics.filter(m => m.name === 'gatekeeper.by_permission_level');
464
+ if (levelMetrics.length > 0) html += buildLabeledTable(levelMetrics, 'permission_level', 'Permission Level');
465
+
466
+ return html;
467
+ }
468
+
469
+ function renderGatekeeper(metrics) {
470
+ const body = document.getElementById('metrics-body-gatekeeper');
471
+ if (!body) return;
472
+
473
+ const total = findVal(metrics, 'gatekeeper.decisions_total');
474
+ const allowed = findVal(metrics, 'gatekeeper.allowed_total');
475
+ const denied = findVal(metrics, 'gatekeeper.denied_total');
476
+ const confirmations = findVal(metrics, 'gatekeeper.confirmations_requested_total');
477
+
478
+ if (total == null || total === 0) {
479
+ body.innerHTML = '<div class="metrics-loading">No Gatekeeper decisions yet</div>';
480
+ return;
481
+ }
482
+
483
+ body.innerHTML = renderGatekeeperStats(metrics, total, allowed, denied, confirmations);
484
+ }
485
+
486
+ function renderLocks(metrics) {
487
+ const body = document.getElementById('metrics-body-locks');
488
+ if (!body) return;
489
+
490
+ const requests = findVal(metrics, 'lock.file.requests_total');
491
+ const active = findVal(metrics, 'lock.file.active_current');
492
+ const timeouts = findVal(metrics, 'lock.file.timeouts_total');
493
+ const waits = findVal(metrics, 'lock.file.concurrent_waits_total');
494
+
495
+ let html = '<div class="metrics-stat-grid">';
496
+ html += statBox('Requests', formatNumber(requests || 0));
497
+ html += statBox('Active', formatNumber(active || 0));
498
+ html += statBox('Timeouts', formatNumber(timeouts || 0));
499
+ html += statBox('Waits', formatNumber(waits || 0));
500
+ html += '</div>';
501
+
502
+ body.innerHTML = html;
503
+ }
504
+
505
+ function renderMetaSystem(metrics) {
506
+ const body = document.getElementById('metrics-body-meta');
507
+ if (!body) return;
508
+
509
+ const registered = findVal(metrics, 'metrics.manager.collectors_registered');
510
+ const disabled = findVal(metrics, 'metrics.manager.disabled_collectors');
511
+ const errors = findVal(metrics, 'metrics.manager.collector_errors_total');
512
+ const duration = findVal(metrics, 'metrics.manager.last_collection_duration_ms');
513
+
514
+ let html = '<div class="metrics-stat-grid">';
515
+ html += statBox('Collectors', formatNumber(registered || 0));
516
+ html += statBox('Disabled', formatNumber(disabled || 0));
517
+ html += statBox('Errors', formatNumber(errors || 0));
518
+ html += statBox('Duration', duration == null ? '-' : formatNumber(duration, 1) + ' ms');
519
+ html += '</div>';
520
+
521
+ if (disabled > 0) {
522
+ html += '<div class="metrics-alert warn">' + disabled + ' collector(s) disabled due to repeated failures</div>';
523
+ }
524
+
525
+ body.innerHTML = html;
526
+ }
527
+
528
+ // ── Chart rendering (uPlot) ──────────────────────────────────────────────
529
+ function extractSeriesValues(name) {
530
+ return historySnapshots.map(s => {
531
+ const m = s.metrics.find(entry => entry.name === name);
532
+ if (!m) return null;
533
+ return typeof m.value === 'number' ? m.value : null;
534
+ });
535
+ }
536
+
537
+ function updateChart(containerId, chartKey, metricNames, labels, formatter) {
538
+ const container = document.getElementById(containerId);
539
+ if (!container || !uPlotAvailable) return;
540
+
541
+ // Defer if container hasn't been laid out yet (hidden tab)
542
+ const width = container.clientWidth;
543
+ if (width < 100) {
544
+ requestAnimationFrame(() => updateChart(containerId, chartKey, metricNames, labels, formatter));
545
+ return;
546
+ }
547
+
548
+ // Build data arrays
549
+ const times = historySnapshots.map(s => Math.floor(new Date(s.timestamp).getTime() / 1000));
550
+ const seriesData = metricNames.map(name => extractSeriesValues(name));
551
+
552
+ // If we already have a chart, update its data instead of recreating
553
+ if (charts[chartKey]) {
554
+ try {
555
+ charts[chartKey].setData([times, ...seriesData]);
556
+ return;
557
+ } catch {
558
+ // If setData fails, fall through to recreate
559
+ charts[chartKey].destroy();
560
+ delete charts[chartKey];
561
+ }
562
+ }
563
+
564
+ const isDark = document.documentElement.dataset.theme === 'dark';
565
+ const colors = ['#3b82f6', '#f59e0b', '#22c55e', '#ef4444'];
566
+
567
+ const opts = {
568
+ width: width,
569
+ height: 200,
570
+ cursor: { show: true },
571
+ legend: { show: true },
572
+ scales: {
573
+ x: { time: true },
574
+ y: { auto: true },
575
+ },
576
+ axes: [
577
+ {
578
+ stroke: isDark ? '#7b93a7' : '#677893',
579
+ grid: { stroke: isDark ? '#2b3445' : '#e0e0e0', width: 1 },
580
+ font: '10px sans-serif',
581
+ size: 40,
582
+ },
583
+ {
584
+ stroke: isDark ? '#7b93a7' : '#677893',
585
+ grid: { stroke: isDark ? '#2b3445' : '#e0e0e0', width: 1 },
586
+ font: '10px sans-serif',
587
+ size: 70,
588
+ values: (u, vals) => vals.map(v => {
589
+ if (v == null) return '';
590
+ return formatter ? formatter(v) : String(v);
591
+ }),
592
+ },
593
+ ],
594
+ series: [
595
+ {},
596
+ ...labels.map((label, i) => ({
597
+ label: label,
598
+ stroke: colors[i % colors.length],
599
+ width: 2,
600
+ fill: colors[i % colors.length] + '18',
601
+ points: { show: true, size: 5, fill: colors[i % colors.length] },
602
+ spanGaps: true,
603
+ })),
604
+ ],
605
+ };
606
+
607
+ try {
608
+ container.innerHTML = '';
609
+ charts[chartKey] = new uPlot(opts, [times, ...seriesData], container); // NOSONAR — uPlot is a third-party library with a lowercase constructor name
610
+ } catch {
611
+ container.innerHTML = '<div class="metrics-loading">Chart unavailable</div>';
612
+ }
613
+ }
614
+
615
+ // ── Helpers ──────────────────────────────────────────────────────────────
616
+
617
+
618
+ function findVal(metrics, name) {
619
+ const m = metrics.find(m => m.name === name);
620
+ if (!m) return null;
621
+ return typeof m.value === 'number' ? m.value : null;
622
+ }
623
+
624
+ function findEntry(metrics, name) {
625
+ return metrics.find(m => m.name === name) || null;
626
+ }
627
+
628
+ function statBox(label, value) {
629
+ return '<div class="metrics-stat">' +
630
+ '<div class="metrics-stat-value">' + escapeHtml(String(value)) + '</div>' +
631
+ '<div class="metrics-stat-label">' + escapeHtml(String(label)) + '</div>' +
632
+ '</div>';
633
+ }
634
+
635
+ function formatMB(mb) {
636
+ if (mb == null) return '-';
637
+ if (mb < 0.01) return '< 0.01 MB';
638
+ if (mb < 1) return (mb * 1024).toFixed(0) + ' KB';
639
+ return mb.toFixed(2) + ' MB';
640
+ }
641
+
642
+ function formatBytes(bytes) {
643
+ if (bytes == null) return '-';
644
+ if (bytes < 1024) return Math.round(bytes) + ' B';
645
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
646
+ if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
647
+ return (bytes / 1073741824).toFixed(2) + ' GB';
648
+ }
649
+
650
+ function formatNumber(n, decimals) {
651
+ if (n == null) return '-';
652
+ if (decimals !== undefined) return Number(n).toFixed(decimals);
653
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
654
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
655
+ return String(Math.round(n * 100) / 100);
656
+ }
657
+
658
+ function formatPercent(v) {
659
+ if (v == null) return '-';
660
+ return (v * 100).toFixed(1) + '%';
661
+ }
662
+
663
+ function fmtMs(v) {
664
+ if (v == null) return '-';
665
+ return Number(v).toFixed(1) + ' ms';
666
+ }
667
+
668
+ function formatDuration(seconds) {
669
+ if (seconds == null) return '-';
670
+ seconds = Math.floor(seconds);
671
+ if (seconds < 60) return seconds + 's';
672
+ if (seconds < 3600) return Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's';
673
+ const h = Math.floor(seconds / 3600);
674
+ const m = Math.floor((seconds % 3600) / 60);
675
+ return h + 'h ' + m + 'm';
676
+ }
677
+
678
+ function escapeHtml(s) {
679
+ if (!s) return '';
680
+ return String(s).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
681
+ }
682
+ })();