test_research4 0.5.0 → 0.5.3

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 (56) hide show
  1. package/index.js +1 -1
  2. package/package.json +5 -4
  3. package/src/client.js +1 -1
  4. package/src/config.js +1 -1
  5. package/src/lib/OrgWatcher.js +1 -1
  6. package/src/lib/auditTrailDownloader.js +1 -1
  7. package/src/lib/handlerRegistry.js +1 -1
  8. package/src/lib/icons.js +1 -1
  9. package/src/lib/initialization.js +1 -1
  10. package/src/lib/logger.js +1 -1
  11. package/src/lib/mcpApps/manifest.js +1 -0
  12. package/src/lib/mcpApps/paths.js +1 -0
  13. package/src/lib/mcpApps/registerResources.js +1 -0
  14. package/src/lib/mcpApps/toolMeta.js +1 -0
  15. package/src/lib/networkUtils.js +1 -1
  16. package/src/lib/salesforceServices.js +1 -1
  17. package/src/lib/telemetry.js +1 -1
  18. package/src/lib/tempManager.js +1 -1
  19. package/src/lib/transport.js +1 -1
  20. package/src/lib/versionManager.js +1 -1
  21. package/src/mcp-server.js +1 -1
  22. package/src/prompts/apex_run_script.js +1 -1
  23. package/src/prompts/call_all_tools.js +1 -1
  24. package/src/prompts/org_onboarding.js +1 -1
  25. package/src/state.js +1 -1
  26. package/src/static/bundles/mcp-apps/get-record/mcp-app.html +275 -0
  27. package/src/static/bundles/mcp-apps/get-setup-audit-trail/mcp-app.html +336 -0
  28. package/src/tools/apex_debug_logs.js +1 -1
  29. package/src/tools/apex_debug_logs.md.pam +1 -1
  30. package/src/tools/deploy_metadata.js +1 -1
  31. package/src/tools/describe_object.js +1 -1
  32. package/src/tools/describe_object.md.pam +1 -1
  33. package/src/tools/execute_queries_and_dml.js +1 -1
  34. package/src/tools/generate_metadata.js +1 -1
  35. package/src/tools/generate_metadata.md.pam +1 -1
  36. package/src/tools/get_apex_class_code_coverage.js +1 -1
  37. package/src/tools/get_recently_viewed_records.js +1 -1
  38. package/src/tools/get_record.js +1 -1
  39. package/src/tools/get_setup_audit_trail.js +1 -1
  40. package/src/tools/invoke_apex_rest_resource.js +1 -1
  41. package/src/tools/run_anonymous_apex.js +1 -1
  42. package/src/tools/run_apex_test.js +1 -1
  43. package/src/tools/run_apex_test.md.pam +1 -1
  44. package/src/tools/trigger_execution_order.js +1 -1
  45. package/src/tools/utils.js +1 -1
  46. package/src/tools/utils.md.pam +1 -1
  47. package/src/utils.js +1 -1
  48. package/src/lib/refreshSobjects.js +0 -1
  49. package/src/lib/taskScheduler.js +0 -1
  50. package/src/lib/uiDataCache.js +0 -1
  51. package/src/prompts/code_modification.js +0 -1
  52. package/src/static/audit_trail_app.html +0 -1067
  53. package/src/static/generate_soql_query_tool_sampling.md.pam +0 -1
  54. package/src/static/get_record_app.html +0 -843
  55. package/src/tools/generate_soql_query.js +0 -1
  56. package/src/tools/generate_soql_query.md.pam +0 -1
@@ -1,1067 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Audit Trail Timeline</title>
7
- <style>
8
- :root {
9
- /* Adapt to both light and dark IDE themes */
10
- color-scheme: light dark;
11
- }
12
-
13
- * {
14
- box-sizing: border-box;
15
- }
16
-
17
- body {
18
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'IBM Plex Sans', sans-serif;
19
- background-color: transparent;
20
- color: inherit;
21
- margin: 0;
22
- padding: 16px;
23
- line-height: 1.5;
24
- }
25
-
26
- #app-container {
27
- max-width: 100%;
28
- margin: 0;
29
- background: transparent;
30
- padding: 0;
31
- }
32
-
33
- #loading {
34
- text-align: center;
35
- opacity: 0.7;
36
- padding: 40px 0;
37
- }
38
-
39
- #error {
40
- background-color: rgba(218, 30, 40, 0.15);
41
- color: #da1e28;
42
- padding: 12px 16px;
43
- border-radius: 4px;
44
- border-left: 3px solid #da1e28;
45
- margin-bottom: 16px;
46
- font-size: 13px;
47
- }
48
-
49
- .timeline-header {
50
- margin-bottom: 20px;
51
- border-bottom: 1px solid;
52
- border-bottom-color: rgba(0, 0, 0, 0.1);
53
- padding-bottom: 12px;
54
- position: sticky;
55
- top: 0;
56
- background-color: transparent;
57
- z-index: 10;
58
- }
59
-
60
- @media (prefers-color-scheme: dark) {
61
- .timeline-header {
62
- border-bottom-color: rgba(255, 255, 255, 0.1);
63
- }
64
- }
65
-
66
- .timeline-header h1 {
67
- margin: 0 0 6px 0;
68
- font-size: 20px;
69
- font-weight: 600;
70
- color: inherit;
71
- }
72
-
73
- .timeline-metadata {
74
- font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
75
- font-size: 11px;
76
- opacity: 0.6;
77
- word-break: break-all;
78
- }
79
-
80
- .controls {
81
- display: flex;
82
- gap: 8px;
83
- margin: 16px 0;
84
- flex-wrap: wrap;
85
- }
86
-
87
- button {
88
- padding: 6px 12px;
89
- background-color: transparent;
90
- color: inherit;
91
- border: 1px solid;
92
- border-color: rgba(0, 0, 0, 0.2);
93
- border-radius: 3px;
94
- cursor: pointer;
95
- font-size: 12px;
96
- font-weight: 500;
97
- font-family: inherit;
98
- transition: all 0.15s ease;
99
- opacity: 0.8;
100
- }
101
-
102
- button:hover {
103
- opacity: 1;
104
- background-color: rgba(0, 0, 0, 0.05);
105
- }
106
-
107
- @media (prefers-color-scheme: dark) {
108
- button {
109
- border-color: rgba(255, 255, 255, 0.2);
110
- }
111
-
112
- button:hover {
113
- background-color: rgba(255, 255, 255, 0.08);
114
- }
115
- }
116
-
117
- button:active {
118
- background-color: rgba(0, 0, 0, 0.1);
119
- }
120
-
121
- @media (prefers-color-scheme: dark) {
122
- button:active {
123
- background-color: rgba(255, 255, 255, 0.15);
124
- }
125
- }
126
-
127
- button:disabled {
128
- opacity: 0.4;
129
- cursor: not-allowed;
130
- }
131
-
132
- .filters-section {
133
- display: flex;
134
- gap: 12px;
135
- margin-bottom: 16px;
136
- flex-wrap: wrap;
137
- align-items: center;
138
- }
139
-
140
- select, input[type="date"] {
141
- padding: 6px 10px;
142
- background-color: transparent;
143
- color: inherit;
144
- border: 1px solid;
145
- border-color: rgba(0, 0, 0, 0.2);
146
- border-radius: 3px;
147
- font-size: 12px;
148
- font-family: inherit;
149
- transition: border-color 0.15s ease;
150
- }
151
-
152
- select:focus, input[type="date"]:focus {
153
- outline: none;
154
- border-color: rgba(0, 0, 0, 0.4);
155
- }
156
-
157
- @media (prefers-color-scheme: dark) {
158
- select, input[type="date"] {
159
- border-color: rgba(255, 255, 255, 0.2);
160
- }
161
-
162
- select:focus, input[type="date"]:focus {
163
- border-color: rgba(255, 255, 255, 0.4);
164
- }
165
- }
166
-
167
- .timeline-container {
168
- position: relative;
169
- padding: 20px 0;
170
- }
171
-
172
- .timeline-date-group {
173
- margin-bottom: 24px;
174
- }
175
-
176
- .timeline-date-header {
177
- font-weight: 600;
178
- font-size: 14px;
179
- margin-bottom: 12px;
180
- opacity: 0.8;
181
- padding: 8px 0;
182
- }
183
-
184
- .timeline-entry {
185
- display: flex;
186
- gap: 12px;
187
- margin-bottom: 12px;
188
- padding: 0;
189
- }
190
-
191
- .timeline-dot {
192
- width: 12px;
193
- height: 12px;
194
- border-radius: 50%;
195
- background-color: #0e639c;
196
- margin-top: 6px;
197
- flex-shrink: 0;
198
- border: 2px solid;
199
- border-color: rgba(0, 0, 0, 0.1);
200
- }
201
-
202
- @media (prefers-color-scheme: dark) {
203
- .timeline-dot {
204
- border-color: rgba(255, 255, 255, 0.1);
205
- }
206
- }
207
-
208
- .timeline-content {
209
- flex: 1;
210
- padding: 8px 12px;
211
- background-color: rgba(0, 0, 0, 0.02);
212
- border-radius: 3px;
213
- border-left: 2px solid rgba(0, 0, 0, 0.1);
214
- }
215
-
216
- @media (prefers-color-scheme: dark) {
217
- .timeline-content {
218
- background-color: rgba(255, 255, 255, 0.02);
219
- border-left-color: rgba(255, 255, 255, 0.1);
220
- }
221
- }
222
-
223
- .timeline-time {
224
- font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
225
- font-size: 11px;
226
- opacity: 0.6;
227
- margin-bottom: 4px;
228
- }
229
-
230
- .timeline-row {
231
- display: flex;
232
- gap: 8px;
233
- align-items: center;
234
- flex-wrap: wrap;
235
- margin-bottom: 4px;
236
- }
237
-
238
- .user-badge {
239
- display: inline-flex;
240
- align-items: center;
241
- gap: 6px;
242
- padding: 3px 8px;
243
- background-color: rgba(0, 0, 0, 0.08);
244
- border-radius: 3px;
245
- font-size: 12px;
246
- font-weight: 500;
247
- }
248
-
249
- @media (prefers-color-scheme: dark) {
250
- .user-badge {
251
- background-color: rgba(255, 255, 255, 0.1);
252
- }
253
- }
254
-
255
- .action-badge {
256
- display: inline-block;
257
- padding: 3px 8px;
258
- border-radius: 3px;
259
- font-size: 11px;
260
- font-weight: 600;
261
- text-transform: uppercase;
262
- }
263
-
264
- .action-badge.created {
265
- background-color: rgba(78, 201, 176, 0.2);
266
- color: #4ec9b0;
267
- }
268
-
269
- .action-badge.changed {
270
- background-color: rgba(220, 220, 170, 0.2);
271
- color: #dcdcaa;
272
- }
273
-
274
- .action-badge.deleted {
275
- background-color: rgba(244, 135, 113, 0.2);
276
- color: #f48771;
277
- }
278
-
279
- .action-detail {
280
- font-size: 12px;
281
- opacity: 0.85;
282
- margin-top: 4px;
283
- }
284
-
285
- .accordion-toggle {
286
- display: flex;
287
- align-items: center;
288
- gap: 6px;
289
- cursor: pointer;
290
- margin-top: 8px;
291
- padding: 6px 0;
292
- font-size: 12px;
293
- opacity: 0.7;
294
- border: none;
295
- background: none;
296
- }
297
-
298
- .accordion-toggle:hover {
299
- opacity: 0.9;
300
- }
301
-
302
- .accordion-toggle::before {
303
- content: '▶';
304
- display: inline-block;
305
- width: 12px;
306
- text-align: center;
307
- font-size: 10px;
308
- transition: transform 0.2s ease;
309
- }
310
-
311
- .accordion-toggle.expanded::before {
312
- content: '▼';
313
- }
314
-
315
- .accordion-content {
316
- margin-left: 20px;
317
- margin-top: 8px;
318
- display: none;
319
- }
320
-
321
- .accordion-content.visible {
322
- display: block;
323
- }
324
-
325
- .accordion-item {
326
- padding: 6px 0;
327
- font-size: 12px;
328
- opacity: 0.8;
329
- }
330
-
331
- .empty-state {
332
- text-align: center;
333
- padding: 40px 20px;
334
- opacity: 0.6;
335
- }
336
-
337
- .timeline-footer {
338
- margin-top: 24px;
339
- padding-top: 12px;
340
- border-top: 1px solid;
341
- border-top-color: rgba(0, 0, 0, 0.1);
342
- font-size: 12px;
343
- opacity: 0.7;
344
- }
345
-
346
- @media (prefers-color-scheme: dark) {
347
- .timeline-footer {
348
- border-top-color: rgba(255, 255, 255, 0.1);
349
- }
350
- }
351
-
352
- .filter-label {
353
- font-size: 12px;
354
- opacity: 0.8;
355
- margin-right: 4px;
356
- }
357
- </style>
358
- </head>
359
- <body>
360
- <div id="app-container">
361
- <div id="loading"><p>Loading timeline...</p></div>
362
- <div id="error" style="display: none;"></div>
363
- <div id="timeline-view" style="display: none;">
364
- <div class="timeline-header">
365
- <h1 id="timeline-title">Audit Trail</h1>
366
- <div class="timeline-metadata" id="timeline-metadata"></div>
367
- </div>
368
-
369
- <div class="filters-section">
370
- <label class="filter-label">Filter by:</label>
371
- <select id="user-filter">
372
- <option value="">All Users</option>
373
- </select>
374
-
375
- <label class="filter-label">Date Range:</label>
376
- <input type="date" id="start-date-filter" />
377
- <span>to</span>
378
- <input type="date" id="end-date-filter" />
379
-
380
- <button id="reset-filters">Reset Filters</button>
381
- </div>
382
-
383
- <div class="timeline-container" id="timeline-container"></div>
384
- <div class="timeline-footer" id="timeline-footer"></div>
385
- </div>
386
- </div>
387
-
388
- <script type="module">
389
- // Inline MCPApp base class for MCP Apps protocol communication
390
- /**
391
- * MCPApp - Base class for MCP Apps UI implementations
392
- */
393
- class MCPApp {
394
- constructor(config) {
395
- this.config = config;
396
- this.ontoolresult = null;
397
- this._requestId = 0;
398
- this._initRequestId = null;
399
- this._pendingRequests = new Map();
400
- this._initialized = false;
401
- this._targetOrigin = null;
402
- this._connected = false;
403
- }
404
-
405
- static _getParentOrigin() {
406
- try {
407
- if (document.referrer) {
408
- const referrerUrl = new URL(document.referrer);
409
- return referrerUrl.origin;
410
- }
411
- } catch (error) {
412
- // Origin cannot be determined from document.referrer
413
- }
414
- return null;
415
- }
416
-
417
- connect() {
418
- if (this._connected) {
419
- console.log('[AuditTrail] Already connected, skipping duplicate call');
420
- return;
421
- }
422
-
423
- console.log('[AuditTrail] Connecting...');
424
-
425
- if (!this._targetOrigin) {
426
- this._targetOrigin = MCPApp._getParentOrigin();
427
- }
428
-
429
- if (!this._targetOrigin) {
430
- console.error('[AuditTrail] Cannot connect: parent origin could not be determined');
431
- return;
432
- }
433
-
434
- // Only mark as connected after target origin is confirmed
435
- this._connected = true;
436
-
437
- window.addEventListener('message', (event) => {
438
- if (event.source !== window.parent) {
439
- return;
440
- }
441
- if (event.origin !== this._targetOrigin) {
442
- return;
443
- }
444
- if (!event.data || typeof event.data !== 'object' || Array.isArray(event.data) || event.data.jsonrpc !== '2.0') {
445
- return;
446
- }
447
- this._handleMessage(event.data);
448
- });
449
-
450
- console.log('[AuditTrail] Sending ui/initialize request');
451
- this._initRequestId = ++this._requestId;
452
- this._sendMessage({
453
- jsonrpc: '2.0',
454
- id: this._initRequestId,
455
- method: 'ui/initialize',
456
- params: {
457
- protocolVersion: '1',
458
- capabilities: {}
459
- }
460
- });
461
- }
462
-
463
- _handleMessage(data) {
464
- if (data.id === this._initRequestId && !this._initialized) {
465
- console.log('[AuditTrail] Received initialization response');
466
- this._initialized = true;
467
-
468
- this._sendMessage({
469
- jsonrpc: '2.0',
470
- method: 'ui/notifications/initialized',
471
- params: {
472
- name: this.config.name,
473
- version: this.config.version
474
- }
475
- });
476
- return;
477
- }
478
-
479
- if (data.method === 'ui/notifications/tool-result') {
480
- const params = data.params || {};
481
- let result = params;
482
- if (!result.content && !result.structuredContent && params.result) {
483
- result = params.result;
484
- }
485
-
486
- if (result.content || result.structuredContent) {
487
- if (this.ontoolresult) {
488
- this.ontoolresult(result);
489
- }
490
- return;
491
- }
492
-
493
- console.warn('[AuditTrail] Tool result missing content and structuredContent');
494
- }
495
-
496
- if (data.method === 'ui/notifications/tool-input') {
497
- const params = data.params || {};
498
- if (params.content || params.structuredContent) {
499
- if (this.ontoolresult) {
500
- this.ontoolresult(params);
501
- }
502
- return;
503
- }
504
- }
505
-
506
- if (data.id && this._pendingRequests.has(data.id)) {
507
- console.log('[AuditTrail] Received response for request ID:', data.id);
508
- const {resolve, reject, timeoutHandle} = this._pendingRequests.get(data.id);
509
- this._pendingRequests.delete(data.id);
510
-
511
- if (timeoutHandle) {
512
- clearTimeout(timeoutHandle);
513
- }
514
-
515
- if (data.error) {
516
- console.error('[AuditTrail] Tool call error:', data.error);
517
- reject(new Error(data.error.message || 'Unknown error'));
518
- } else {
519
- console.log('[AuditTrail] Tool call succeeded');
520
- resolve(data.result);
521
- }
522
- }
523
- }
524
-
525
- async callServerTool(toolCall) {
526
- const id = ++this._requestId;
527
- console.log('[AuditTrail] Calling server tool:', toolCall.name, 'with ID:', id);
528
-
529
- return new Promise((resolve, reject) => {
530
- const timeoutMs = 30000;
531
- const timeoutHandle = setTimeout(() => {
532
- this._pendingRequests.delete(id);
533
- console.error('[AuditTrail] Tool call timeout after', timeoutMs, 'ms');
534
- reject(new Error('Tool call timeout after ' + timeoutMs + 'ms'));
535
- }, timeoutMs);
536
-
537
- this._pendingRequests.set(id, {resolve, reject, timeoutHandle});
538
-
539
- this._sendMessage({
540
- jsonrpc: '2.0',
541
- method: 'ui/callTool',
542
- id,
543
- params: {
544
- name: toolCall.name,
545
- arguments: toolCall.arguments || {}
546
- }
547
- });
548
- });
549
- }
550
-
551
- _sendMessage(data) {
552
- console.log('[AuditTrail] Sending message:', data);
553
- if (!this._targetOrigin) {
554
- console.error('[AuditTrail] Cannot send message: target origin not set');
555
- return;
556
- }
557
- window.parent.postMessage(data, this._targetOrigin);
558
- }
559
- }
560
-
561
- const app = new MCPApp({ name: 'audit-trail-app', version: '1.0.0' });
562
-
563
- let currentRecords = [];
564
- let filteredRecords = [];
565
-
566
- function processToolResult(result) {
567
- try {
568
- if (result.isError) {
569
- showError(result.content?.[0]?.text || 'Unknown error');
570
- return;
571
- }
572
-
573
- let data;
574
-
575
- if (result.structuredContent) {
576
- data = result.structuredContent;
577
- } else if (result.content) {
578
- const textContent = Array.isArray(result.content)
579
- ? result.content.find(c => c.type === 'text')?.text
580
- : result.content;
581
- if (!textContent) {
582
- showError('No data returned from tool');
583
- return;
584
- }
585
- console.log('[ProcessResult] Parsing JSON from text content');
586
- data = typeof textContent === 'string' ? JSON.parse(textContent) : textContent;
587
- } else {
588
- showError('No data returned from tool');
589
- return;
590
- }
591
-
592
- renderTimeline(data);
593
- } catch (error) {
594
- showError(`Failed to parse audit trail data: ${error.message}`);
595
- console.error('[ProcessResult] Error:', error);
596
- }
597
- }
598
-
599
- app.ontoolresult = (result) => {
600
- console.log('[App] ontoolresult callback triggered:', result);
601
- processToolResult(result);
602
- };
603
-
604
- console.log('[Init] Establishing MCP Apps connection...');
605
- app.connect();
606
- console.log('[Init] App.connect() called');
607
-
608
- if (window.__initialData) {
609
- console.log('[Init] Found initial data injected by server');
610
- if (window.__initialData.error) {
611
- // Error state injected due to expired/unknown cache ID
612
- console.log('[Init] Injected data is an error state:', window.__initialData.message);
613
- showError(window.__initialData.message);
614
- } else {
615
- processToolResult({
616
- content: [{type: 'text', text: JSON.stringify(window.__initialData)}],
617
- structuredContent: window.__initialData
618
- });
619
- }
620
- }
621
-
622
- if (window.__auditTrailData) {
623
- console.log('[Init] Found audit trail data in window.__auditTrailData');
624
- processToolResult(window.__auditTrailData);
625
- }
626
-
627
- // Only start timeout when no initial data (app opened without prefetched data, e.g. from resource link before tool completed)
628
- let timeoutId = null;
629
- if (!window.__initialData && !window.__auditTrailData) {
630
- timeoutId = setTimeout(() => {
631
- if (document.getElementById('loading').style.display !== 'none') {
632
- console.error('[Timeout] No data received from server after 10 seconds');
633
- showError('Timeout: No data received from server after 10 seconds. Check browser console for debug logs. The host may not support MCP Apps or there may be a configuration issue.');
634
- }
635
- }, 10000);
636
- }
637
-
638
- function showError(message) {
639
- console.error('[UI] Showing error:', message);
640
- document.getElementById('loading').style.display = 'none';
641
- document.getElementById('timeline-view').style.display = 'none';
642
- const errorEl = document.getElementById('error');
643
- errorEl.textContent = message;
644
- errorEl.style.display = 'block';
645
- }
646
-
647
- function renderTimeline(data) {
648
- console.log('[Render] Rendering timeline with data:', data);
649
-
650
- if (timeoutId) {
651
- clearTimeout(timeoutId);
652
- }
653
-
654
- const { metadataName, records, error } = data;
655
-
656
- if (error) {
657
- showError(data.message || 'Error fetching audit trail');
658
- return;
659
- }
660
-
661
- if (!Array.isArray(records) || records.length === 0) {
662
- renderEmptyState(metadataName);
663
- return;
664
- }
665
-
666
- currentRecords = records;
667
- applyFilters();
668
- setupFilterListeners();
669
-
670
- document.getElementById('loading').style.display = 'none';
671
- document.getElementById('error').style.display = 'none';
672
- document.getElementById('timeline-view').style.display = 'block';
673
-
674
- const title = metadataName ? `Audit Trail for ${metadataName}` : 'Audit Trail';
675
- document.getElementById('timeline-title').textContent = title;
676
- document.getElementById('timeline-metadata').textContent = `Total records: ${records.length}`;
677
-
678
- populateUserFilter();
679
- renderTimelineContent();
680
- }
681
-
682
- function renderEmptyState(metadataName) {
683
- if (timeoutId) {
684
- clearTimeout(timeoutId);
685
- }
686
- document.getElementById('loading').style.display = 'none';
687
- document.getElementById('error').style.display = 'none';
688
- document.getElementById('timeline-view').style.display = 'block';
689
-
690
- const title = metadataName ? `Audit Trail for ${metadataName}` : 'Audit Trail';
691
- document.getElementById('timeline-title').textContent = title;
692
- document.getElementById('timeline-metadata').textContent = 'No records found';
693
-
694
- const container = document.getElementById('timeline-container');
695
- container.innerHTML = '<div class="empty-state"><p>No audit trail records found for this metadata.</p></div>';
696
- }
697
-
698
- function populateUserFilter() {
699
- const users = [...new Set(currentRecords.map(r => r.userId || r.username || 'Unknown'))];
700
- const select = document.getElementById('user-filter');
701
- const currentValue = select.value;
702
-
703
- // Keep existing options except the first one (All Users)
704
- const existingOptions = Array.from(select.options);
705
- while (select.options.length > 1) {
706
- select.remove(1);
707
- }
708
-
709
- users.forEach(user => {
710
- const option = document.createElement('option');
711
- option.value = user;
712
- option.textContent = user;
713
- select.appendChild(option);
714
- });
715
-
716
- select.value = currentValue;
717
- }
718
-
719
- function setupFilterListeners() {
720
- const userFilter = document.getElementById('user-filter');
721
- const startDateFilter = document.getElementById('start-date-filter');
722
- const endDateFilter = document.getElementById('end-date-filter');
723
- const resetBtn = document.getElementById('reset-filters');
724
-
725
- if (userFilter && !userFilter._listenerAttached) {
726
- userFilter.addEventListener('change', applyFilters);
727
- userFilter._listenerAttached = true;
728
- }
729
-
730
- if (startDateFilter && !startDateFilter._listenerAttached) {
731
- startDateFilter.addEventListener('change', applyFilters);
732
- startDateFilter._listenerAttached = true;
733
- }
734
-
735
- if (endDateFilter && !endDateFilter._listenerAttached) {
736
- endDateFilter.addEventListener('change', applyFilters);
737
- endDateFilter._listenerAttached = true;
738
- }
739
-
740
- if (resetBtn && !resetBtn._listenerAttached) {
741
- resetBtn.addEventListener('click', () => {
742
- userFilter.value = '';
743
- startDateFilter.value = '';
744
- endDateFilter.value = '';
745
- applyFilters();
746
- });
747
- resetBtn._listenerAttached = true;
748
- }
749
- }
750
-
751
- function isValidDate(dateStr) {
752
- if (!dateStr || typeof dateStr !== 'string') return false;
753
- const d = new Date(dateStr);
754
- return d instanceof Date && !isNaN(d);
755
- }
756
-
757
- function getLocalDateString(dateStr) {
758
- const date = new Date(dateStr);
759
- return date.toLocaleDateString('en-CA');
760
- }
761
-
762
- function applyFilters() {
763
- const userValue = document.getElementById('user-filter').value;
764
- const startDate = document.getElementById('start-date-filter').value;
765
- const endDate = document.getElementById('end-date-filter').value;
766
-
767
- filteredRecords = currentRecords.filter(record => {
768
- // Prefer explicit userId/username if present, fall back to generic `user`
769
- const recordUser = record.userId || record.username || record.user || '';
770
-
771
- // Date filter: support both `CreatedDate` and `date` shapes
772
- if (startDate || endDate) {
773
- const rawDate = record.CreatedDate || record.date;
774
- // Include record if date is missing or invalid
775
- if (!isValidDate(rawDate)) return true;
776
- const recordDate = getLocalDateString(rawDate);
777
- if (startDate && recordDate < startDate) return false;
778
- if (endDate && recordDate > endDate) return false;
779
- }
780
-
781
- // User filter
782
- if (userValue && recordUser !== userValue) return false;
783
-
784
- return true;
785
- });
786
-
787
- console.log('[Filters] Applied filters, showing', filteredRecords.length, 'of', currentRecords.length, 'records');
788
- renderTimelineContent();
789
- }
790
-
791
- function renderTimelineContent() {
792
- if (filteredRecords.length === 0) {
793
- const container = document.getElementById('timeline-container');
794
- container.innerHTML = '<div class="empty-state"><p>No records match the selected filters.</p></div>';
795
- updateFooter();
796
- return;
797
- }
798
-
799
- const container = document.getElementById('timeline-container');
800
- container.innerHTML = '';
801
-
802
- // Group records by date
803
- const grouped = groupRecordsByDate(filteredRecords);
804
- const dates = Object.keys(grouped).sort().reverse();
805
-
806
- dates.forEach(dateStr => {
807
- const dateGroup = grouped[dateStr];
808
- const dateElement = document.createElement('div');
809
- dateElement.className = 'timeline-date-group';
810
-
811
- const dateHeader = document.createElement('div');
812
- dateHeader.className = 'timeline-date-header';
813
- dateHeader.textContent = formatDateHeader(dateStr);
814
-
815
- dateElement.appendChild(dateHeader);
816
-
817
- // Group consecutive records by user
818
- const userGroups = groupConsecutiveByUser(dateGroup);
819
-
820
- userGroups.forEach((group, index) => {
821
- if (group.length === 1) {
822
- // Single record
823
- const entry = createTimelineEntry(group[0]);
824
- dateElement.appendChild(entry);
825
- } else {
826
- // Multiple consecutive records from same user - show accordion
827
- const entry = createAccordionEntry(group);
828
- dateElement.appendChild(entry);
829
- }
830
- });
831
-
832
- container.appendChild(dateElement);
833
- });
834
-
835
- updateFooter();
836
- }
837
-
838
- function groupRecordsByDate(records) {
839
- const grouped = {};
840
- records.forEach(record => {
841
- const date = record.CreatedDate ? getLocalDateString(record.CreatedDate) : 'Unknown';
842
- if (!grouped[date]) {
843
- grouped[date] = [];
844
- }
845
- grouped[date].push(record);
846
- });
847
- return grouped;
848
- }
849
-
850
- function groupConsecutiveByUser(records) {
851
- const groups = [];
852
- let currentGroup = [];
853
- let lastUser = null;
854
-
855
- records.forEach(record => {
856
- const currentUser = record.userId || record.username || 'Unknown';
857
- if (lastUser !== currentUser && currentGroup.length > 0) {
858
- groups.push(currentGroup);
859
- currentGroup = [];
860
- }
861
- currentGroup.push(record);
862
- lastUser = currentUser;
863
- });
864
-
865
- if (currentGroup.length > 0) {
866
- groups.push(currentGroup);
867
- }
868
-
869
- return groups;
870
- }
871
-
872
- function createTimelineEntry(record) {
873
- const div = document.createElement('div');
874
- div.className = 'timeline-entry';
875
-
876
- const dot = document.createElement('div');
877
- dot.className = 'timeline-dot';
878
- div.appendChild(dot);
879
-
880
- const content = document.createElement('div');
881
- content.className = 'timeline-content';
882
-
883
- const time = document.createElement('div');
884
- time.className = 'timeline-time';
885
- time.textContent = formatTime(record.CreatedDate);
886
- content.appendChild(time);
887
-
888
- const row = document.createElement('div');
889
- row.className = 'timeline-row';
890
-
891
- const user = record.userId || record.username || 'Unknown';
892
- const userBadge = document.createElement('span');
893
- userBadge.className = 'user-badge';
894
- userBadge.textContent = user;
895
- row.appendChild(userBadge);
896
-
897
- const action = record.action || 'Unknown';
898
- const actionType = normalizeActionType(action);
899
- const actionBadge = document.createElement('span');
900
- actionBadge.className = `action-badge ${actionType}`;
901
- actionBadge.textContent = actionType.toUpperCase();
902
- row.appendChild(actionBadge);
903
-
904
- content.appendChild(row);
905
-
906
- if (record.detail || record.description) {
907
- const detail = document.createElement('div');
908
- detail.className = 'action-detail';
909
- detail.textContent = record.detail || record.description || action;
910
- content.appendChild(detail);
911
- }
912
-
913
- div.appendChild(content);
914
- return div;
915
- }
916
-
917
- function createAccordionEntry(records) {
918
- const div = document.createElement('div');
919
- div.className = 'timeline-entry';
920
-
921
- const dot = document.createElement('div');
922
- dot.className = 'timeline-dot';
923
- div.appendChild(dot);
924
-
925
- const content = document.createElement('div');
926
- content.className = 'timeline-content';
927
-
928
- const firstRecord = records[0];
929
- const user = firstRecord.userId || firstRecord.username || 'Unknown';
930
-
931
- // Show first record
932
- const time = document.createElement('div');
933
- time.className = 'timeline-time';
934
- time.textContent = formatTime(firstRecord.CreatedDate);
935
- content.appendChild(time);
936
-
937
- const row = document.createElement('div');
938
- row.className = 'timeline-row';
939
-
940
- const userBadge = document.createElement('span');
941
- userBadge.className = 'user-badge';
942
- userBadge.textContent = user;
943
- row.appendChild(userBadge);
944
-
945
- const action = firstRecord.action || 'Unknown';
946
- const actionType = normalizeActionType(action);
947
- const actionBadge = document.createElement('span');
948
- actionBadge.className = `action-badge ${actionType}`;
949
- actionBadge.textContent = actionType.toUpperCase();
950
- row.appendChild(actionBadge);
951
-
952
- content.appendChild(row);
953
-
954
- if (firstRecord.detail || firstRecord.description) {
955
- const detail = document.createElement('div');
956
- detail.className = 'action-detail';
957
- detail.textContent = firstRecord.detail || firstRecord.description || action;
958
- content.appendChild(detail);
959
- }
960
-
961
- // Add accordion toggle
962
- const toggleBtn = document.createElement('button');
963
- toggleBtn.className = 'accordion-toggle';
964
- toggleBtn.textContent = `${records.length - 1} more changes by ${user}`;
965
-
966
- const accordionContent = document.createElement('div');
967
- accordionContent.className = 'accordion-content';
968
-
969
- records.slice(1).forEach((record, idx) => {
970
- const item = document.createElement('div');
971
- item.className = 'accordion-item';
972
- const time = formatTime(record.CreatedDate);
973
- const action = record.action || 'Unknown';
974
- const actionType = normalizeActionType(action);
975
-
976
- // Create structure with innerHTML (safe - no user data)
977
- item.innerHTML = `<span style="opacity: 0.6;"></span> <span class="action-badge ${actionType}" style="font-size: 10px;"></span> <span class="record-detail"></span>`;
978
-
979
- // Populate with textContent (safe for user-controlled data)
980
- item.querySelector('span:first-child').textContent = time;
981
- item.querySelector('.action-badge').textContent = actionType.toUpperCase();
982
- item.querySelector('.record-detail').textContent = record.detail || record.description || action;
983
-
984
- accordionContent.appendChild(item);
985
- });
986
-
987
- toggleBtn.addEventListener('click', () => {
988
- toggleBtn.classList.toggle('expanded');
989
- accordionContent.classList.toggle('visible');
990
- });
991
-
992
- content.appendChild(toggleBtn);
993
- content.appendChild(accordionContent);
994
-
995
- div.appendChild(content);
996
- return div;
997
- }
998
-
999
- function normalizeActionType(action) {
1000
- const lower = (action || '').toLowerCase();
1001
- if (lower.includes('created') || lower.includes('create')) return 'created';
1002
- if (lower.includes('deleted') || lower.includes('delete')) return 'deleted';
1003
- return 'changed';
1004
- }
1005
-
1006
- function formatTime(dateStr) {
1007
- try {
1008
- const date = new Date(dateStr);
1009
- const hours = String(date.getHours()).padStart(2, '0');
1010
- const minutes = String(date.getMinutes()).padStart(2, '0');
1011
- const seconds = String(date.getSeconds()).padStart(2, '0');
1012
- return `${hours}:${minutes}:${seconds}`;
1013
- } catch {
1014
- return dateStr || 'Unknown time';
1015
- }
1016
- }
1017
-
1018
- function escapeHtml(text) {
1019
- const map = {
1020
- '&': '&amp;',
1021
- '<': '&lt;',
1022
- '>': '&gt;',
1023
- '"': '&quot;',
1024
- "'": '&#039;'
1025
- };
1026
- return String(text).replace(/[&<>"']/g, m => map[m]);
1027
- }
1028
-
1029
- function formatDateHeader(dateStr) {
1030
- try {
1031
- const date = new Date(dateStr);
1032
- const options = { year: 'numeric', month: 'short', day: 'numeric' };
1033
- return date.toLocaleDateString('en-US', options);
1034
- } catch {
1035
- return escapeHtml(dateStr || 'Unknown date');
1036
- }
1037
- }
1038
-
1039
- function updateFooter() {
1040
- const footer = document.getElementById('timeline-footer');
1041
- footer.innerHTML = '';
1042
-
1043
- if (filteredRecords.length === 0) {
1044
- return;
1045
- }
1046
-
1047
- const uniqueUsers = new Set(filteredRecords.map(r => r.userId || r.username || 'Unknown'));
1048
- const dates = filteredRecords.map(r => r.CreatedDate).filter(Boolean).sort();
1049
- const startDate = dates.length > 0 ? formatDateHeader(dates[0].split('T')[0]) : 'N/A';
1050
- const endDate = dates.length > 0 ? formatDateHeader(dates[dates.length - 1].split('T')[0]) : 'N/A';
1051
-
1052
- const totalDiv = document.createElement('div');
1053
- totalDiv.textContent = `Total changes: ${filteredRecords.length}`;
1054
-
1055
- const usersDiv = document.createElement('div');
1056
- usersDiv.textContent = `Unique users: ${uniqueUsers.size}`;
1057
-
1058
- const dateRangeDiv = document.createElement('div');
1059
- dateRangeDiv.textContent = `Date range: ${startDate} to ${endDate}`;
1060
-
1061
- footer.appendChild(totalDiv);
1062
- footer.appendChild(usersDiv);
1063
- footer.appendChild(dateRangeDiv);
1064
- }
1065
- </script>
1066
- </body>
1067
- </html>