@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.
- package/CHANGELOG.md +52 -61
- package/README.github.md +2 -2
- package/README.md +2 -2
- package/README.md.backup +284 -224
- package/README.npm.md +2 -2
- package/dist/cache/LRUCache.d.ts +3 -0
- package/dist/cache/LRUCache.d.ts.map +1 -1
- package/dist/cache/LRUCache.js +36 -26
- package/dist/config/env.d.ts +14 -4
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +20 -6
- package/dist/di/Container.d.ts +21 -0
- package/dist/di/Container.d.ts.map +1 -1
- package/dist/di/Container.js +250 -53
- package/dist/elements/BaseElement.d.ts.map +1 -1
- package/dist/elements/BaseElement.js +5 -10
- package/dist/elements/base/BaseElementManager.d.ts +22 -0
- package/dist/elements/base/BaseElementManager.d.ts.map +1 -1
- package/dist/elements/base/BaseElementManager.js +47 -7
- package/dist/elements/memories/Memory.d.ts +1 -0
- package/dist/elements/memories/Memory.d.ts.map +1 -1
- package/dist/elements/memories/Memory.js +12 -8
- package/dist/elements/memories/MemoryManager.d.ts.map +1 -1
- package/dist/elements/memories/MemoryManager.js +23 -42
- package/dist/elements/memories/MemorySearchIndex.js +2 -2
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.d.ts.map +1 -1
- package/dist/generated/version.js +3 -3
- package/dist/handlers/EnhancedIndexHandler.js +6 -6
- package/dist/handlers/element-crud/listElements.d.ts +2 -0
- package/dist/handlers/element-crud/listElements.d.ts.map +1 -1
- package/dist/handlers/element-crud/listElements.js +3 -1
- package/dist/handlers/mcp-aql/Gatekeeper.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/Gatekeeper.js +23 -17
- package/dist/handlers/mcp-aql/MCPAQLHandler.d.ts +14 -0
- package/dist/handlers/mcp-aql/MCPAQLHandler.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/MCPAQLHandler.js +110 -14
- package/dist/handlers/mcp-aql/OperationRouter.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/OperationRouter.js +13 -1
- package/dist/handlers/mcp-aql/OperationSchema.d.ts +7 -0
- package/dist/handlers/mcp-aql/OperationSchema.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/OperationSchema.js +52 -1
- package/dist/handlers/mcp-aql/evaluatePermission.d.ts +53 -0
- package/dist/handlers/mcp-aql/evaluatePermission.d.ts.map +1 -0
- package/dist/handlers/mcp-aql/evaluatePermission.js +132 -0
- package/dist/handlers/mcp-aql/policies/ToolClassification.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/policies/ToolClassification.js +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/logging/LogHooks.js +11 -11
- package/dist/logging/LogManager.d.ts +0 -2
- package/dist/logging/LogManager.d.ts.map +1 -1
- package/dist/logging/LogManager.js +1 -3
- package/dist/logging/sinks/MemoryLogSink.d.ts +2 -0
- package/dist/logging/sinks/MemoryLogSink.d.ts.map +1 -1
- package/dist/logging/sinks/MemoryLogSink.js +12 -3
- package/dist/logging/types.d.ts +0 -2
- package/dist/logging/types.d.ts.map +1 -1
- package/dist/logging/types.js +1 -1
- package/dist/metrics/GatekeeperMetricsTracker.d.ts +32 -0
- package/dist/metrics/GatekeeperMetricsTracker.d.ts.map +1 -0
- package/dist/metrics/GatekeeperMetricsTracker.js +42 -0
- package/dist/metrics/MetricsManager.d.ts +47 -0
- package/dist/metrics/MetricsManager.d.ts.map +1 -0
- package/dist/metrics/MetricsManager.js +232 -0
- package/dist/metrics/OperationMetricsTracker.d.ts +32 -0
- package/dist/metrics/OperationMetricsTracker.d.ts.map +1 -0
- package/dist/metrics/OperationMetricsTracker.js +53 -0
- package/dist/metrics/collectors/DefaultElementProviderCollector.d.ts +27 -0
- package/dist/metrics/collectors/DefaultElementProviderCollector.d.ts.map +1 -0
- package/dist/metrics/collectors/DefaultElementProviderCollector.js +69 -0
- package/dist/metrics/collectors/FileLockManagerCollector.d.ts +16 -0
- package/dist/metrics/collectors/FileLockManagerCollector.d.ts.map +1 -0
- package/dist/metrics/collectors/FileLockManagerCollector.js +51 -0
- package/dist/metrics/collectors/GatekeeperMetricsCollector.d.ts +16 -0
- package/dist/metrics/collectors/GatekeeperMetricsCollector.d.ts.map +1 -0
- package/dist/metrics/collectors/GatekeeperMetricsCollector.js +76 -0
- package/dist/metrics/collectors/LRUCacheCollector.d.ts +18 -0
- package/dist/metrics/collectors/LRUCacheCollector.d.ts.map +1 -0
- package/dist/metrics/collectors/LRUCacheCollector.js +95 -0
- package/dist/metrics/collectors/OperationMetricsCollector.d.ts +16 -0
- package/dist/metrics/collectors/OperationMetricsCollector.d.ts.map +1 -0
- package/dist/metrics/collectors/OperationMetricsCollector.js +80 -0
- package/dist/metrics/collectors/OperationalTelemetryCollector.d.ts +17 -0
- package/dist/metrics/collectors/OperationalTelemetryCollector.d.ts.map +1 -0
- package/dist/metrics/collectors/OperationalTelemetryCollector.js +26 -0
- package/dist/metrics/collectors/PerformanceMonitorCollector.d.ts +14 -0
- package/dist/metrics/collectors/PerformanceMonitorCollector.d.ts.map +1 -0
- package/dist/metrics/collectors/PerformanceMonitorCollector.js +141 -0
- package/dist/metrics/collectors/SecurityMonitorCollector.d.ts +21 -0
- package/dist/metrics/collectors/SecurityMonitorCollector.d.ts.map +1 -0
- package/dist/metrics/collectors/SecurityMonitorCollector.js +56 -0
- package/dist/metrics/collectors/SecurityTelemetryCollector.d.ts +15 -0
- package/dist/metrics/collectors/SecurityTelemetryCollector.d.ts.map +1 -0
- package/dist/metrics/collectors/SecurityTelemetryCollector.js +112 -0
- package/dist/metrics/collectors/TriggerMetricsTrackerCollector.d.ts +16 -0
- package/dist/metrics/collectors/TriggerMetricsTrackerCollector.d.ts.map +1 -0
- package/dist/metrics/collectors/TriggerMetricsTrackerCollector.js +26 -0
- package/dist/metrics/collectors/index.d.ts +11 -0
- package/dist/metrics/collectors/index.d.ts.map +1 -0
- package/dist/metrics/collectors/index.js +11 -0
- package/dist/metrics/sinks/MemoryMetricsSink.d.ts +22 -0
- package/dist/metrics/sinks/MemoryMetricsSink.d.ts.map +1 -0
- package/dist/metrics/sinks/MemoryMetricsSink.js +121 -0
- package/dist/metrics/types.d.ts +98 -0
- package/dist/metrics/types.d.ts.map +1 -0
- package/dist/metrics/types.js +24 -0
- package/dist/portfolio/DefaultElementProvider.d.ts.map +1 -1
- package/dist/portfolio/DefaultElementProvider.js +1 -7
- package/dist/portfolio/EnhancedIndexManager.d.ts.map +1 -1
- package/dist/portfolio/EnhancedIndexManager.js +18 -18
- package/dist/portfolio/NLPScoringManager.d.ts.map +1 -1
- package/dist/portfolio/NLPScoringManager.js +5 -9
- package/dist/portfolio/PortfolioIndexManager.js +2 -2
- package/dist/portfolio/RelationshipManager.js +2 -2
- package/dist/portfolio/VerbTriggerManager.d.ts.map +1 -1
- package/dist/portfolio/VerbTriggerManager.js +5 -19
- package/dist/portfolio/config/IndexConfig.d.ts.map +1 -1
- package/dist/portfolio/config/IndexConfig.js +1 -12
- package/dist/portfolio/enhanced-index/ElementDefinitionBuilder.d.ts.map +1 -1
- package/dist/portfolio/enhanced-index/ElementDefinitionBuilder.js +3 -15
- package/dist/portfolio/enhanced-index/SemanticRelationshipService.d.ts.map +1 -1
- package/dist/portfolio/enhanced-index/SemanticRelationshipService.js +2 -16
- package/dist/portfolio/types/RelationshipTypes.d.ts.map +1 -1
- package/dist/portfolio/types/RelationshipTypes.js +3 -17
- package/dist/security/audit/config/suppressions.d.ts.map +1 -1
- package/dist/security/audit/config/suppressions.js +36 -8
- package/dist/security/constants.d.ts.map +1 -1
- package/dist/security/constants.js +10 -6
- package/dist/security/fileLockManager.d.ts.map +1 -1
- package/dist/security/fileLockManager.js +8 -6
- package/dist/security/secureYamlParser.d.ts.map +1 -1
- package/dist/security/secureYamlParser.js +1 -13
- package/dist/security/securityMonitor.d.ts +2 -1
- package/dist/security/securityMonitor.d.ts.map +1 -1
- package/dist/security/securityMonitor.js +14 -3
- package/dist/security/telemetry/SecurityTelemetry.d.ts +16 -0
- package/dist/security/telemetry/SecurityTelemetry.d.ts.map +1 -1
- package/dist/security/telemetry/SecurityTelemetry.js +30 -2
- package/dist/security/tokenManager.d.ts +3 -0
- package/dist/security/tokenManager.d.ts.map +1 -1
- package/dist/security/tokenManager.js +13 -5
- package/dist/security/validation/BackgroundValidator.d.ts.map +1 -1
- package/dist/security/validation/BackgroundValidator.js +7 -7
- package/dist/server/startup.d.ts.map +1 -1
- package/dist/server/startup.js +8 -24
- package/dist/server/tools/MCPAQLTools.js +4 -1
- package/dist/services/ActivationStore.d.ts.map +1 -1
- package/dist/services/ActivationStore.js +9 -3
- package/dist/services/FileWatchService.d.ts +1 -0
- package/dist/services/FileWatchService.d.ts.map +1 -1
- package/dist/services/FileWatchService.js +83 -48
- package/dist/services/MetadataService.d.ts.map +1 -1
- package/dist/services/MetadataService.js +7 -2
- package/dist/services/query/ElementQueryService.d.ts.map +1 -1
- package/dist/services/query/ElementQueryService.js +1 -41
- package/dist/services/query/PaginationService.d.ts.map +1 -1
- package/dist/services/query/PaginationService.js +1 -14
- package/dist/services/query/SortService.d.ts.map +1 -1
- package/dist/services/query/SortService.js +1 -6
- package/dist/services/validation/ValidationService.d.ts.map +1 -1
- package/dist/services/validation/ValidationService.js +3 -8
- package/dist/storage/ElementStorageLayer.d.ts.map +1 -1
- package/dist/storage/ElementStorageLayer.js +5 -2
- package/dist/storage/MemoryStorageLayer.d.ts.map +1 -1
- package/dist/storage/MemoryStorageLayer.js +5 -2
- package/dist/telemetry/OperationalTelemetry.js +2 -2
- package/dist/utils/EventDeduplicator.d.ts +44 -0
- package/dist/utils/EventDeduplicator.d.ts.map +1 -0
- package/dist/utils/EventDeduplicator.js +93 -0
- package/dist/utils/FileLock.d.ts.map +1 -1
- package/dist/utils/FileLock.js +1 -9
- package/dist/utils/PerformanceMonitor.d.ts.map +1 -1
- package/dist/utils/PerformanceMonitor.js +5 -5
- package/dist/utils/SlidingWindowRateLimiter.d.ts +13 -0
- package/dist/utils/SlidingWindowRateLimiter.d.ts.map +1 -0
- package/dist/utils/SlidingWindowRateLimiter.js +23 -0
- package/dist/web/console/IngestRoutes.d.ts +84 -0
- package/dist/web/console/IngestRoutes.d.ts.map +1 -0
- package/dist/web/console/IngestRoutes.js +252 -0
- package/dist/web/console/LeaderElection.d.ts +89 -0
- package/dist/web/console/LeaderElection.d.ts.map +1 -0
- package/dist/web/console/LeaderElection.js +205 -0
- package/dist/web/console/LeaderForwardingSink.d.ts +61 -0
- package/dist/web/console/LeaderForwardingSink.d.ts.map +1 -0
- package/dist/web/console/LeaderForwardingSink.js +197 -0
- package/dist/web/console/SessionNames.d.ts +46 -0
- package/dist/web/console/SessionNames.d.ts.map +1 -0
- package/dist/web/console/SessionNames.js +257 -0
- package/dist/web/console/UnifiedConsole.d.ts +64 -0
- package/dist/web/console/UnifiedConsole.d.ts.map +1 -0
- package/dist/web/console/UnifiedConsole.js +119 -0
- package/dist/web/contentPipeline.d.ts +58 -0
- package/dist/web/contentPipeline.d.ts.map +1 -0
- package/dist/web/contentPipeline.js +112 -0
- package/dist/web/portDiscovery.d.ts +58 -0
- package/dist/web/portDiscovery.d.ts.map +1 -0
- package/dist/web/portDiscovery.js +143 -0
- package/dist/web/public/app.js +148 -60
- package/dist/web/public/logs.js +638 -0
- package/dist/web/public/metrics.js +682 -0
- package/dist/web/public/permissions.js +394 -0
- package/dist/web/public/sessions.js +369 -0
- package/dist/web/routes/healthRoutes.d.ts +16 -0
- package/dist/web/routes/healthRoutes.d.ts.map +1 -0
- package/dist/web/routes/healthRoutes.js +29 -0
- package/dist/web/routes/logRoutes.d.ts +18 -0
- package/dist/web/routes/logRoutes.d.ts.map +1 -0
- package/dist/web/routes/logRoutes.js +126 -0
- package/dist/web/routes/metricsRoutes.d.ts +17 -0
- package/dist/web/routes/metricsRoutes.d.ts.map +1 -0
- package/dist/web/routes/metricsRoutes.js +90 -0
- package/dist/web/routes/permissionRoutes.d.ts +16 -0
- package/dist/web/routes/permissionRoutes.d.ts.map +1 -0
- package/dist/web/routes/permissionRoutes.js +133 -0
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +309 -339
- package/dist/web/server.d.ts +21 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +42 -4
- package/dist/web/sinks/WebSSELogSink.d.ts +15 -0
- package/dist/web/sinks/WebSSELogSink.d.ts.map +1 -0
- package/dist/web/sinks/WebSSELogSink.js +22 -0
- package/dist/web/sinks/WebSSEMetricsSink.d.ts +16 -0
- package/dist/web/sinks/WebSSEMetricsSink.d.ts.map +1 -0
- package/dist/web/sinks/WebSSEMetricsSink.js +23 -0
- package/package.json +2 -2
- package/server.json +2 -2
- package/dist/constants/version.d.ts +0 -3
- package/dist/constants/version.d.ts.map +0 -1
- package/dist/constants/version.js +0 -4
- package/dist/logging/sinks/SSELogSink.d.ts +0 -35
- package/dist/logging/sinks/SSELogSink.d.ts.map +0 -1
- package/dist/logging/sinks/SSELogSink.js +0 -181
- package/dist/logging/viewer/viewerHtml.d.ts +0 -8
- package/dist/logging/viewer/viewerHtml.d.ts.map +0 -1
- package/dist/logging/viewer/viewerHtml.js +0 -204
- 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">🔗</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">✕ 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">✕</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
|
+
'🔗 ' + 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('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
|
|
637
|
+
}
|
|
638
|
+
})();
|