@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,638 @@
1
+ /**
2
+ * DollhouseMCP Log Viewer — High-performance virtual-scrolling log viewer
3
+ *
4
+ * Architecture:
5
+ * - Circular buffer (JS-side, 10,000 entries)
6
+ * - Virtual scrolling: only renders ~50 visible DOM rows
7
+ * - RAF batching: incoming SSE entries accumulate, processed once per frame
8
+ * - Client-side filtering on JS buffer, not DOM
9
+ * - Smart auto-scroll with user scroll detection
10
+ * - Detail modal card for full log entry inspection
11
+ * - Multi-select with shift-click for bulk copy
12
+ */
13
+
14
+ (() => {
15
+ const BUFFER_SIZE = 10000;
16
+ const ROW_HEIGHT = 22;
17
+ const OVERSCAN = 5;
18
+ const RECONNECT_DELAY_MS = 3000;
19
+ const SEARCH_DEBOUNCE_MS = 300;
20
+
21
+ // ── State ────────────────────────────────────────────────────────────────
22
+ const buffer = [];
23
+ let filteredIndices = null;
24
+ let eventSource = null;
25
+ let paused = false;
26
+ let autoScroll = true;
27
+ let searchTimer = null;
28
+ let pendingEntries = [];
29
+ let rafScheduled = false;
30
+
31
+ // Selection state
32
+ const selectedIds = new Set();
33
+ let lastClickedIndex = -1; // for shift-click range select
34
+
35
+ // Filter state
36
+ let filterCategory = '';
37
+ let filterLevel = 'info'; // Default to info — excludes debug noise from view
38
+ let filterSource = '';
39
+ let filterMessage = '';
40
+ let filterCorrelationId = '';
41
+
42
+ // ── DOM references ─────────────────────────────────────────────────────
43
+ let viewport, scrollSpacer, jumpBtn, statusDot, statusText, entryCountEl;
44
+ let categorySelect, levelSelect, sourceInput, searchInput, pauseBtn, clearBtn;
45
+ let detailModal, copySelectedBtn, selectCountEl;
46
+ const rowPool = [];
47
+
48
+ // ── Public API ───────────────────────────────────────────────────────────
49
+ globalThis.DollhouseConsole = globalThis.DollhouseConsole || {};
50
+ globalThis.DollhouseConsole.logs = {
51
+ init: initLogViewer,
52
+ destroy: destroyLogViewer,
53
+ refresh: () => {
54
+ requestAnimationFrame(() => {
55
+ renderViewport();
56
+ if (autoScroll) scrollToBottom();
57
+ });
58
+ },
59
+ };
60
+
61
+ function initLogViewer() {
62
+ const container = document.getElementById('log-viewer-root');
63
+ if (!container || container.dataset.initialized === 'true') return;
64
+ container.dataset.initialized = 'true';
65
+
66
+ buildDOM(container);
67
+ bindEvents();
68
+ connectSSE();
69
+
70
+ requestAnimationFrame(() => {
71
+ renderViewport();
72
+ if (autoScroll) scrollToBottom();
73
+ });
74
+ }
75
+
76
+ function destroyLogViewer() {
77
+ if (eventSource) {
78
+ eventSource.close();
79
+ eventSource = null;
80
+ }
81
+ }
82
+
83
+ // ── DOM construction ─────────────────────────────────────────────────────
84
+ function buildDOM(container) {
85
+ container.innerHTML = `
86
+ <div class="log-viewer">
87
+ <div class="log-controls">
88
+ <div class="log-filter-group">
89
+ <label for="log-category">Cat</label>
90
+ <select id="log-category">
91
+ <option value="">All</option>
92
+ <option value="application">application</option>
93
+ <option value="security">security</option>
94
+ <option value="performance">performance</option>
95
+ <option value="telemetry">telemetry</option>
96
+ </select>
97
+ </div>
98
+ <div class="log-filter-group">
99
+ <label for="log-level">Level</label>
100
+ <select id="log-level">
101
+ <option value="">All</option>
102
+ <option value="debug">debug</option>
103
+ <option value="info" selected>info</option>
104
+ <option value="warn">warn</option>
105
+ <option value="error">error</option>
106
+ </select>
107
+ </div>
108
+ <div class="log-filter-group">
109
+ <label for="log-source">Source</label>
110
+ <input type="text" id="log-source" class="log-search" placeholder="source filter...">
111
+ </div>
112
+ <div class="log-filter-group">
113
+ <label for="log-search">Search</label>
114
+ <input type="search" id="log-search" class="log-search" placeholder="message search...">
115
+ </div>
116
+ </div>
117
+ <div class="log-status-bar">
118
+ <span class="log-status-indicator">
119
+ <span class="log-status-dot" id="log-status-dot"></span>
120
+ <span id="log-status-text">connecting...</span>
121
+ </span>
122
+ <button class="log-action-btn" id="log-pause-btn">Pause</button>
123
+ <button class="log-action-btn" id="log-clear-btn">Clear</button>
124
+ <button class="log-action-btn" id="log-copy-selected-btn" style="display:none">Copy Selected (<span id="log-select-count">0</span>)</button>
125
+ <button class="log-action-btn" id="log-deselect-btn" style="display:none">Deselect All</button>
126
+ <span class="log-entry-count" id="log-entry-count">0 entries</span>
127
+ </div>
128
+ <div class="log-trace-banner" id="log-trace-banner" style="display:none">
129
+ <span class="log-trace-banner-icon">&#x1f517;</span>
130
+ <span>Tracing request: <code id="log-trace-id"></code></span>
131
+ <span id="log-trace-count"></span>
132
+ <button class="log-trace-clear" id="log-trace-clear">&#x2715; Clear trace</button>
133
+ </div>
134
+ <div class="log-viewport" id="log-viewport">
135
+ <div class="log-scroll-spacer" id="log-scroll-spacer"></div>
136
+ </div>
137
+ <button class="log-jump-bottom" id="log-jump-bottom">Jump to bottom</button>
138
+ </div>
139
+
140
+ <div class="log-detail-modal" id="log-detail-modal" hidden>
141
+ <div class="log-detail-backdrop" id="log-detail-backdrop"></div>
142
+ <div class="log-detail-card">
143
+ <div class="log-detail-card-header">
144
+ <span class="log-detail-card-title" id="log-detail-title">Log Entry</span>
145
+ <div class="log-detail-card-actions">
146
+ <button class="log-action-btn" id="log-detail-copy-text">Copy Text</button>
147
+ <button class="log-action-btn" id="log-detail-copy-json">Copy JSON</button>
148
+ <button class="log-detail-close" id="log-detail-close">&#x2715;</button>
149
+ </div>
150
+ </div>
151
+ <div class="log-detail-card-body" id="log-detail-body"></div>
152
+ </div>
153
+ </div>
154
+ `;
155
+
156
+ viewport = document.getElementById('log-viewport');
157
+ scrollSpacer = document.getElementById('log-scroll-spacer');
158
+ jumpBtn = document.getElementById('log-jump-bottom');
159
+ statusDot = document.getElementById('log-status-dot');
160
+ statusText = document.getElementById('log-status-text');
161
+ entryCountEl = document.getElementById('log-entry-count');
162
+ categorySelect = document.getElementById('log-category');
163
+ levelSelect = document.getElementById('log-level');
164
+ if (levelSelect && filterLevel) levelSelect.value = filterLevel; // Sync dropdown with default
165
+ sourceInput = document.getElementById('log-source');
166
+ searchInput = document.getElementById('log-search');
167
+ pauseBtn = document.getElementById('log-pause-btn');
168
+ clearBtn = document.getElementById('log-clear-btn');
169
+ detailModal = document.getElementById('log-detail-modal');
170
+ copySelectedBtn = document.getElementById('log-copy-selected-btn');
171
+ selectCountEl = document.getElementById('log-select-count');
172
+ }
173
+
174
+ function bindEvents() {
175
+ viewport.addEventListener('scroll', onScroll);
176
+ jumpBtn.addEventListener('click', () => {
177
+ autoScroll = true;
178
+ scrollToBottom();
179
+ jumpBtn.classList.remove('visible');
180
+ });
181
+
182
+ pauseBtn.addEventListener('click', () => {
183
+ paused = !paused;
184
+ pauseBtn.textContent = paused ? 'Resume' : 'Pause';
185
+ pauseBtn.classList.toggle('active', paused);
186
+ if (!paused) scheduleRender();
187
+ });
188
+
189
+ clearBtn.addEventListener('click', () => {
190
+ buffer.length = 0;
191
+ filteredIndices = null;
192
+ selectedIds.clear();
193
+ updateSelectionUI();
194
+ applyFilters();
195
+ renderViewport();
196
+ });
197
+
198
+ // Copy selected
199
+ copySelectedBtn.addEventListener('click', copySelectedEntries);
200
+ document.getElementById('log-deselect-btn').addEventListener('click', () => {
201
+ selectedIds.clear();
202
+ updateSelectionUI();
203
+ renderViewport();
204
+ });
205
+
206
+ // Detail modal
207
+ document.getElementById('log-detail-close').addEventListener('click', closeDetailModal);
208
+ document.getElementById('log-detail-backdrop').addEventListener('click', closeDetailModal);
209
+ document.getElementById('log-detail-copy-text').addEventListener('click', () => copyDetailAs('text'));
210
+ document.getElementById('log-detail-copy-json').addEventListener('click', () => copyDetailAs('json'));
211
+
212
+ // Keyboard: Escape closes modal
213
+ document.addEventListener('keydown', (e) => {
214
+ if (e.key === 'Escape' && !detailModal.hidden) {
215
+ closeDetailModal();
216
+ }
217
+ });
218
+
219
+ categorySelect.addEventListener('change', () => { filterCategory = categorySelect.value; applyFilters(); });
220
+ levelSelect.addEventListener('change', () => { filterLevel = levelSelect.value; applyFilters(); });
221
+ sourceInput.addEventListener('input', () => { filterSource = sourceInput.value; applyFiltersDebounced(); });
222
+ searchInput.addEventListener('input', () => { filterMessage = searchInput.value; applyFiltersDebounced(); });
223
+ document.getElementById('log-trace-clear').addEventListener('click', clearTraceFilter);
224
+ }
225
+
226
+ function setTraceFilter(correlationId) {
227
+ filterCorrelationId = correlationId;
228
+ const banner = document.getElementById('log-trace-banner');
229
+ const traceIdEl = document.getElementById('log-trace-id');
230
+ const traceCountEl = document.getElementById('log-trace-count');
231
+ traceIdEl.textContent = correlationId;
232
+ banner.style.display = '';
233
+ applyFilters();
234
+ const count = getVisibleCount();
235
+ traceCountEl.textContent = '(' + count + ' entries)';
236
+ }
237
+
238
+ function clearTraceFilter() {
239
+ filterCorrelationId = '';
240
+ document.getElementById('log-trace-banner').style.display = 'none';
241
+ applyFilters();
242
+ }
243
+
244
+ // ── SSE connection ───────────────────────────────────────────────────────
245
+ function connectSSE() {
246
+ if (eventSource) eventSource.close();
247
+ setStatus('reconnecting');
248
+ // Pass current filters to server for SSE-level filtering (reduces bandwidth)
249
+ const params = new URLSearchParams();
250
+ if (filterCategory) params.set('category', filterCategory);
251
+ if (filterLevel) params.set('level', filterLevel);
252
+ const qs = params.toString();
253
+ eventSource = new EventSource('/api/logs/stream' + (qs ? '?' + qs : ''));
254
+
255
+ eventSource.onopen = () => setStatus('connected');
256
+
257
+ eventSource.onmessage = (event) => {
258
+ try {
259
+ const entry = JSON.parse(event.data);
260
+ pendingEntries.push(entry);
261
+ if (!rafScheduled) {
262
+ rafScheduled = true;
263
+ requestAnimationFrame(processPending);
264
+ }
265
+ } catch { /* malformed */ }
266
+ };
267
+
268
+ eventSource.onerror = () => {
269
+ setStatus('disconnected');
270
+ eventSource.close();
271
+ eventSource = null;
272
+ setTimeout(connectSSE, RECONNECT_DELAY_MS);
273
+ };
274
+ }
275
+
276
+ // ── RAF batch processing ─────────────────────────────────────────────────
277
+ function appendToBuffer(entry) {
278
+ if (buffer.length >= BUFFER_SIZE) {
279
+ const removed = buffer.shift();
280
+ if (removed) selectedIds.delete(removed.id);
281
+ if (filteredIndices !== null) {
282
+ filteredIndices = filteredIndices.map(i => i - 1).filter(i => i >= 0);
283
+ }
284
+ }
285
+ buffer.push(entry);
286
+ if (filteredIndices !== null && matchesFilters(entry)) {
287
+ filteredIndices.push(buffer.length - 1);
288
+ }
289
+ }
290
+
291
+ function processPending() {
292
+ rafScheduled = false;
293
+ if (pendingEntries.length === 0) return;
294
+
295
+ const entries = pendingEntries;
296
+ pendingEntries = [];
297
+ for (const entry of entries) appendToBuffer(entry);
298
+
299
+ if (!paused) {
300
+ updateEntryCount();
301
+ renderViewport();
302
+ if (autoScroll) scrollToBottom();
303
+ }
304
+ }
305
+
306
+ // ── Filtering ────────────────────────────────────────────────────────────
307
+ const LEVEL_PRIORITY = { debug: 0, info: 1, warn: 2, error: 3 };
308
+
309
+ function matchesFilters(entry) {
310
+ if (filterCorrelationId && entry.correlationId !== filterCorrelationId) return false;
311
+ if (filterCategory && entry.category !== filterCategory) return false;
312
+ if (filterLevel && (LEVEL_PRIORITY[entry.level] || 0) < (LEVEL_PRIORITY[filterLevel] || 0)) return false;
313
+ if (filterSource && !entry.source.toLowerCase().includes(filterSource.toLowerCase())) return false;
314
+ if (filterMessage && !entry.message.toLowerCase().includes(filterMessage.toLowerCase())) return false;
315
+ return true;
316
+ }
317
+
318
+ function applyFilters() {
319
+ const hasFilter = filterCategory || filterLevel || filterSource || filterMessage || filterCorrelationId;
320
+ if (hasFilter) {
321
+ filteredIndices = [];
322
+ for (let i = 0; i < buffer.length; i++) {
323
+ if (matchesFilters(buffer[i])) filteredIndices.push(i);
324
+ }
325
+ } else {
326
+ filteredIndices = null;
327
+ }
328
+ updateEntryCount();
329
+ renderViewport();
330
+ }
331
+
332
+ function applyFiltersDebounced() {
333
+ clearTimeout(searchTimer);
334
+ searchTimer = setTimeout(applyFilters, SEARCH_DEBOUNCE_MS);
335
+ }
336
+
337
+ // ── Virtual scroll rendering ─────────────────────────────────────────────
338
+ function getVisibleCount() {
339
+ return filteredIndices === null ? buffer.length : filteredIndices.length;
340
+ }
341
+
342
+ function getEntry(visibleIndex) {
343
+ const bufferIndex = filteredIndices === null ? visibleIndex : filteredIndices[visibleIndex];
344
+ return buffer[bufferIndex];
345
+ }
346
+
347
+ function renderViewport() {
348
+ const totalItems = getVisibleCount();
349
+ scrollSpacer.style.height = (totalItems * ROW_HEIGHT) + 'px';
350
+
351
+ const scrollTop = viewport.scrollTop;
352
+ let viewHeight = viewport.clientHeight;
353
+ if (viewHeight === 0) viewHeight = 600;
354
+
355
+ const startIdx = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
356
+ const endIdx = Math.min(totalItems, Math.ceil((scrollTop + viewHeight) / ROW_HEIGHT) + OVERSCAN);
357
+ const needed = endIdx - startIdx;
358
+
359
+ while (rowPool.length < needed) {
360
+ const row = document.createElement('div');
361
+ row.className = 'log-entry';
362
+ row.addEventListener('click', onRowClick);
363
+ scrollSpacer.appendChild(row);
364
+ rowPool.push(row);
365
+ }
366
+
367
+ for (let i = 0; i < rowPool.length; i++) {
368
+ const row = rowPool[i];
369
+ if (i < needed) {
370
+ const visIdx = startIdx + i;
371
+ const entry = getEntry(visIdx);
372
+ if (entry) {
373
+ updateRow(row, entry, visIdx);
374
+ row.style.display = '';
375
+ } else {
376
+ row.style.display = 'none';
377
+ }
378
+ } else {
379
+ row.style.display = 'none';
380
+ }
381
+ }
382
+ }
383
+
384
+ function updateRow(row, entry, visibleIndex) {
385
+ const isSelected = selectedIds.has(entry.id);
386
+ row.style.top = (visibleIndex * ROW_HEIGHT) + 'px';
387
+ row.style.height = ROW_HEIGHT + 'px';
388
+ row.dataset.entryId = entry.id;
389
+ row.dataset.visibleIndex = visibleIndex;
390
+ row.className = 'log-entry level-' + escapeHtml(entry.level) + (isSelected ? ' selected' : '');
391
+ row.innerHTML = renderCompactEntry(entry, isSelected);
392
+ }
393
+
394
+ function renderCompactEntry(entry, isSelected) {
395
+ const time = formatTime(entry.timestamp);
396
+ const src = escapeHtml(entry.source || '').slice(0, 30);
397
+ const msg = escapeHtml(entry.message || '').slice(0, 300);
398
+ const checkbox = '<span class="log-checkbox' + (isSelected ? ' checked' : '') + '"></span>';
399
+ const corrBadge = entry.correlationId
400
+ ? '<span class="log-corr-badge" title="Click to trace this request" data-correlation-id="' + escapeHtml(entry.correlationId) + '">' + escapeHtml(entry.correlationId.slice(-8)) + '</span>'
401
+ : '<span class="log-corr-badge empty"></span>';
402
+ return checkbox +
403
+ '<span class="log-time">' + time + '</span>' +
404
+ '<span class="log-level ' + escapeHtml(entry.level) + '">' + escapeHtml(entry.level) + '</span>' +
405
+ '<span class="log-category">' + escapeHtml(entry.category) + '</span>' +
406
+ corrBadge +
407
+ '<span class="log-source">' + src + '</span>' +
408
+ '<span class="log-message">' + msg + '</span>';
409
+ }
410
+
411
+ // ── Detail modal ─────────────────────────────────────────────────────────
412
+ let activeDetailEntry = null;
413
+
414
+ function openDetailModal(entry) {
415
+ activeDetailEntry = entry;
416
+ const title = document.getElementById('log-detail-title');
417
+ const body = document.getElementById('log-detail-body');
418
+
419
+ title.textContent = entry.level.toUpperCase() + ' — ' + (entry.source || 'unknown');
420
+
421
+ let html = '';
422
+ html += detailField('Timestamp', entry.timestamp);
423
+ html += detailField('ID', entry.id);
424
+ html += detailField('Category', escapeHtml(entry.category));
425
+ html += detailField('Level', '<span class="log-level ' + escapeHtml(entry.level) + '">' + escapeHtml(entry.level) + '</span>');
426
+ html += detailField('Source', escapeHtml(entry.source || ''));
427
+ html += detailField('Message', escapeHtml(entry.message || ''));
428
+
429
+ if (entry.correlationId) {
430
+ html += detailField('Correlation ID',
431
+ '<a href="#" class="log-trace-link" data-correlation-id="' + escapeHtml(entry.correlationId) + '">' +
432
+ '&#x1f517; ' + escapeHtml(entry.correlationId) + '</a>');
433
+ }
434
+
435
+ if (entry.data && Object.keys(entry.data).length > 0) {
436
+ html += '<div class="log-detail-section">';
437
+ html += '<div class="log-detail-section-title">Data</div>';
438
+ html += '<pre class="log-detail-pre">' + escapeHtml(JSON.stringify(entry.data, null, 2)) + '</pre>';
439
+ html += '</div>';
440
+ }
441
+
442
+ if (entry.error) {
443
+ html += '<div class="log-detail-section">';
444
+ html += '<div class="log-detail-section-title">Error</div>';
445
+ html += detailField('Name', escapeHtml(entry.error.name || ''));
446
+ html += detailField('Message', escapeHtml(entry.error.message || ''));
447
+ if (entry.error.stack) {
448
+ html += '<pre class="log-detail-pre">' + escapeHtml(entry.error.stack) + '</pre>';
449
+ }
450
+ html += '</div>';
451
+ }
452
+
453
+ body.innerHTML = html;
454
+ detailModal.hidden = false;
455
+
456
+ // Bind trace link click in modal
457
+ const traceLink = body.querySelector('.log-trace-link');
458
+ if (traceLink) {
459
+ traceLink.addEventListener('click', (e) => {
460
+ e.preventDefault();
461
+ const corrId = traceLink.dataset.correlationId;
462
+ if (corrId) {
463
+ closeDetailModal();
464
+ setTraceFilter(corrId);
465
+ }
466
+ });
467
+ }
468
+ }
469
+
470
+ function closeDetailModal() {
471
+ detailModal.hidden = true;
472
+ activeDetailEntry = null;
473
+ }
474
+
475
+ function detailField(label, value) {
476
+ return '<div class="log-detail-field">' +
477
+ '<span class="log-detail-field-label">' + label + '</span>' +
478
+ '<span class="log-detail-field-value">' + value + '</span>' +
479
+ '</div>';
480
+ }
481
+
482
+ function copyDetailAs(format) {
483
+ if (!activeDetailEntry) return;
484
+ let text;
485
+ if (format === 'json') {
486
+ text = JSON.stringify(activeDetailEntry, null, 2);
487
+ } else {
488
+ text = formatEntryAsText(activeDetailEntry);
489
+ }
490
+ copyToClipboard(text);
491
+ }
492
+
493
+ // ── Selection & copy ─────────────────────────────────────────────────────
494
+ function handleTraceClick(e) {
495
+ const clickedTrace = e.target.closest('.log-corr-badge');
496
+ if (!clickedTrace) return false;
497
+ e.stopPropagation();
498
+ const corrId = clickedTrace.dataset.correlationId;
499
+ if (corrId) {
500
+ if (filterCorrelationId === corrId) clearTraceFilter();
501
+ else setTraceFilter(corrId);
502
+ }
503
+ return true;
504
+ }
505
+
506
+ function handleSelectionClick(e, entryId, visIdx) {
507
+ const clickedCheckbox = e.target.closest('.log-checkbox');
508
+
509
+ if (e.shiftKey && lastClickedIndex >= 0) {
510
+ const start = Math.min(lastClickedIndex, visIdx);
511
+ const end = Math.max(lastClickedIndex, visIdx);
512
+ for (let i = start; i <= end; i++) {
513
+ const entry = getEntry(i);
514
+ if (entry) selectedIds.add(entry.id);
515
+ }
516
+ } else if (clickedCheckbox || e.ctrlKey || e.metaKey) {
517
+ if (selectedIds.has(entryId)) selectedIds.delete(entryId);
518
+ else selectedIds.add(entryId);
519
+ } else {
520
+ const entry = buffer.find(e => e.id === entryId);
521
+ if (entry) openDetailModal(entry);
522
+ return;
523
+ }
524
+ lastClickedIndex = visIdx;
525
+ updateSelectionUI();
526
+ renderViewport();
527
+ }
528
+
529
+ function onRowClick(e) {
530
+ const row = e.currentTarget;
531
+ const entryId = row.dataset.entryId;
532
+ const visIdx = Number.parseInt(row.dataset.visibleIndex, 10);
533
+ if (!entryId) return;
534
+ if (handleTraceClick(e)) return;
535
+ handleSelectionClick(e, entryId, visIdx);
536
+ }
537
+
538
+ function updateSelectionUI() {
539
+ const count = selectedIds.size;
540
+ copySelectedBtn.style.display = count > 0 ? '' : 'none';
541
+ document.getElementById('log-deselect-btn').style.display = count > 0 ? '' : 'none';
542
+ selectCountEl.textContent = String(count);
543
+ }
544
+
545
+ function copySelectedEntries() {
546
+ if (selectedIds.size === 0) return;
547
+ const entries = buffer.filter(e => selectedIds.has(e.id));
548
+ // Sort by timestamp
549
+ entries.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
550
+ const text = entries.map(formatEntryAsText).join('\n\n');
551
+ copyToClipboard(text);
552
+ }
553
+
554
+ // ── Scroll & status ──────────────────────────────────────────────────────
555
+ function onScroll() {
556
+ const atBottom = viewport.scrollTop + viewport.clientHeight >= viewport.scrollHeight - ROW_HEIGHT * 2;
557
+ autoScroll = atBottom;
558
+ jumpBtn.classList.toggle('visible', !atBottom && getVisibleCount() > 0);
559
+ renderViewport();
560
+ }
561
+
562
+ function scheduleRender() {
563
+ if (!rafScheduled) {
564
+ rafScheduled = true;
565
+ requestAnimationFrame(() => {
566
+ rafScheduled = false;
567
+ updateEntryCount();
568
+ renderViewport();
569
+ if (autoScroll) scrollToBottom();
570
+ });
571
+ }
572
+ }
573
+
574
+ // ── Helpers ──────────────────────────────────────────────────────────────
575
+ function scrollToBottom() {
576
+ viewport.scrollTop = viewport.scrollHeight;
577
+ }
578
+
579
+ function setStatus(status) {
580
+ statusDot.className = 'log-status-dot ' + status;
581
+ statusText.textContent = status;
582
+ }
583
+
584
+ function updateEntryCount() {
585
+ const total = buffer.length;
586
+ const visible = getVisibleCount();
587
+ entryCountEl.textContent = filteredIndices === null
588
+ ? total + ' entries'
589
+ : visible + ' / ' + total + ' entries';
590
+ }
591
+
592
+ function formatTime(iso) {
593
+ if (!iso) return '';
594
+ const d = new Date(iso);
595
+ return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) +
596
+ '.' + String(d.getMilliseconds()).padStart(3, '0');
597
+ }
598
+
599
+ function formatEntryAsText(entry) {
600
+ let text = '[' + entry.timestamp + '] ' +
601
+ entry.level.toUpperCase() + ' [' + entry.category + '] ' +
602
+ (entry.source || '') + ': ' +
603
+ (entry.message || '');
604
+ if (entry.correlationId) text += '\n correlationId: ' + entry.correlationId;
605
+ if (entry.data && Object.keys(entry.data).length > 0) {
606
+ text += '\n data: ' + JSON.stringify(entry.data, null, 2).split('\n').join('\n ');
607
+ }
608
+ if (entry.error) {
609
+ text += '\n error: ' + (entry.error.name || '') + ': ' + (entry.error.message || '');
610
+ if (entry.error.stack) text += '\n ' + entry.error.stack.split('\n').join('\n ');
611
+ }
612
+ return text;
613
+ }
614
+
615
+ function copyToClipboard(text) {
616
+ navigator.clipboard.writeText(text).then(() => {
617
+ // Brief visual feedback on status bar
618
+ const prev = statusText.textContent;
619
+ statusText.textContent = 'Copied!';
620
+ setTimeout(() => { statusText.textContent = prev; }, 1500);
621
+ }).catch(() => {
622
+ // Fallback for older browsers
623
+ const ta = document.createElement('textarea');
624
+ ta.value = text;
625
+ ta.style.position = 'fixed';
626
+ ta.style.opacity = '0';
627
+ document.body.appendChild(ta);
628
+ ta.select();
629
+ document.execCommand('copy'); // NOSONAR — intentional fallback for browsers without navigator.clipboard support
630
+ ta.remove();
631
+ });
632
+ }
633
+
634
+ function escapeHtml(s) {
635
+ if (!s) return '';
636
+ return s.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
637
+ }
638
+ })();