claude-memory-layer 1.0.6 → 1.0.7

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.
@@ -0,0 +1,745 @@
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>Code Memory Dashboard</title>
7
+ <style>
8
+ :root {
9
+ --bg-primary: #1a1a2e;
10
+ --bg-secondary: #16213e;
11
+ --bg-card: #0f3460;
12
+ --text-primary: #e6e6e6;
13
+ --text-secondary: #a0a0a0;
14
+ --accent: #e94560;
15
+ --accent-secondary: #533483;
16
+ --success: #4ade80;
17
+ --warning: #fbbf24;
18
+ --border: rgba(255, 255, 255, 0.1);
19
+ }
20
+
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ }
26
+
27
+ body {
28
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
29
+ background: var(--bg-primary);
30
+ color: var(--text-primary);
31
+ min-height: 100vh;
32
+ line-height: 1.6;
33
+ }
34
+
35
+ .container {
36
+ max-width: 1400px;
37
+ margin: 0 auto;
38
+ padding: 20px;
39
+ }
40
+
41
+ header {
42
+ display: flex;
43
+ justify-content: space-between;
44
+ align-items: center;
45
+ padding: 20px 0;
46
+ border-bottom: 1px solid var(--border);
47
+ margin-bottom: 30px;
48
+ }
49
+
50
+ .logo {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 12px;
54
+ font-size: 1.5rem;
55
+ font-weight: 600;
56
+ }
57
+
58
+ .logo-icon {
59
+ font-size: 2rem;
60
+ }
61
+
62
+ .refresh-btn {
63
+ background: var(--bg-card);
64
+ border: 1px solid var(--border);
65
+ color: var(--text-primary);
66
+ padding: 10px 20px;
67
+ border-radius: 8px;
68
+ cursor: pointer;
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 8px;
72
+ transition: all 0.2s;
73
+ }
74
+
75
+ .refresh-btn:hover {
76
+ background: var(--accent);
77
+ }
78
+
79
+ .refresh-btn.loading {
80
+ opacity: 0.6;
81
+ pointer-events: none;
82
+ }
83
+
84
+ .stats-grid {
85
+ display: grid;
86
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
87
+ gap: 20px;
88
+ margin-bottom: 30px;
89
+ }
90
+
91
+ .stat-card {
92
+ background: var(--bg-card);
93
+ border-radius: 12px;
94
+ padding: 24px;
95
+ text-align: center;
96
+ border: 1px solid var(--border);
97
+ transition: transform 0.2s;
98
+ }
99
+
100
+ .stat-card:hover {
101
+ transform: translateY(-2px);
102
+ }
103
+
104
+ .stat-value {
105
+ font-size: 2.5rem;
106
+ font-weight: 700;
107
+ color: var(--accent);
108
+ margin-bottom: 8px;
109
+ }
110
+
111
+ .stat-label {
112
+ color: var(--text-secondary);
113
+ font-size: 0.9rem;
114
+ text-transform: uppercase;
115
+ letter-spacing: 1px;
116
+ }
117
+
118
+ .search-container {
119
+ margin-bottom: 30px;
120
+ }
121
+
122
+ .search-input {
123
+ width: 100%;
124
+ padding: 16px 20px;
125
+ background: var(--bg-secondary);
126
+ border: 1px solid var(--border);
127
+ border-radius: 12px;
128
+ color: var(--text-primary);
129
+ font-size: 1rem;
130
+ }
131
+
132
+ .search-input::placeholder {
133
+ color: var(--text-secondary);
134
+ }
135
+
136
+ .search-input:focus {
137
+ outline: none;
138
+ border-color: var(--accent);
139
+ }
140
+
141
+ .main-grid {
142
+ display: grid;
143
+ grid-template-columns: 1fr 1fr;
144
+ gap: 30px;
145
+ }
146
+
147
+ @media (max-width: 900px) {
148
+ .main-grid {
149
+ grid-template-columns: 1fr;
150
+ }
151
+ }
152
+
153
+ .section {
154
+ background: var(--bg-secondary);
155
+ border-radius: 12px;
156
+ padding: 24px;
157
+ border: 1px solid var(--border);
158
+ }
159
+
160
+ .section-title {
161
+ font-size: 1.2rem;
162
+ margin-bottom: 20px;
163
+ display: flex;
164
+ align-items: center;
165
+ gap: 10px;
166
+ }
167
+
168
+ .session-list {
169
+ list-style: none;
170
+ }
171
+
172
+ .session-item {
173
+ padding: 16px;
174
+ background: var(--bg-card);
175
+ border-radius: 8px;
176
+ margin-bottom: 12px;
177
+ cursor: pointer;
178
+ transition: all 0.2s;
179
+ border: 1px solid transparent;
180
+ }
181
+
182
+ .session-item:hover {
183
+ border-color: var(--accent);
184
+ }
185
+
186
+ .session-header {
187
+ display: flex;
188
+ justify-content: space-between;
189
+ align-items: center;
190
+ margin-bottom: 8px;
191
+ }
192
+
193
+ .session-id {
194
+ font-family: monospace;
195
+ font-size: 0.9rem;
196
+ color: var(--accent);
197
+ }
198
+
199
+ .session-time {
200
+ color: var(--text-secondary);
201
+ font-size: 0.85rem;
202
+ }
203
+
204
+ .session-meta {
205
+ display: flex;
206
+ gap: 16px;
207
+ color: var(--text-secondary);
208
+ font-size: 0.85rem;
209
+ }
210
+
211
+ .shared-item {
212
+ padding: 16px;
213
+ background: var(--bg-card);
214
+ border-radius: 8px;
215
+ margin-bottom: 12px;
216
+ display: flex;
217
+ justify-content: space-between;
218
+ align-items: center;
219
+ }
220
+
221
+ .shared-type {
222
+ display: flex;
223
+ align-items: center;
224
+ gap: 10px;
225
+ }
226
+
227
+ .shared-icon {
228
+ font-size: 1.5rem;
229
+ }
230
+
231
+ .shared-count {
232
+ background: var(--accent);
233
+ padding: 4px 12px;
234
+ border-radius: 20px;
235
+ font-weight: 600;
236
+ }
237
+
238
+ .timeline-container {
239
+ margin-top: 30px;
240
+ }
241
+
242
+ .timeline-bar {
243
+ display: flex;
244
+ gap: 4px;
245
+ height: 60px;
246
+ align-items: flex-end;
247
+ }
248
+
249
+ .timeline-day {
250
+ flex: 1;
251
+ background: var(--accent);
252
+ border-radius: 4px 4px 0 0;
253
+ min-height: 4px;
254
+ transition: opacity 0.2s;
255
+ position: relative;
256
+ }
257
+
258
+ .timeline-day:hover {
259
+ opacity: 0.8;
260
+ }
261
+
262
+ .timeline-day:hover::after {
263
+ content: attr(data-tooltip);
264
+ position: absolute;
265
+ bottom: 100%;
266
+ left: 50%;
267
+ transform: translateX(-50%);
268
+ background: var(--bg-card);
269
+ padding: 8px 12px;
270
+ border-radius: 6px;
271
+ font-size: 0.8rem;
272
+ white-space: nowrap;
273
+ z-index: 10;
274
+ }
275
+
276
+ .timeline-labels {
277
+ display: flex;
278
+ justify-content: space-between;
279
+ margin-top: 8px;
280
+ color: var(--text-secondary);
281
+ font-size: 0.8rem;
282
+ }
283
+
284
+ .endless-status {
285
+ display: flex;
286
+ align-items: center;
287
+ gap: 12px;
288
+ padding: 16px;
289
+ background: var(--bg-card);
290
+ border-radius: 8px;
291
+ margin-bottom: 16px;
292
+ }
293
+
294
+ .status-indicator {
295
+ width: 12px;
296
+ height: 12px;
297
+ border-radius: 50%;
298
+ }
299
+
300
+ .status-indicator.active {
301
+ background: var(--success);
302
+ box-shadow: 0 0 10px var(--success);
303
+ }
304
+
305
+ .status-indicator.inactive {
306
+ background: var(--text-secondary);
307
+ }
308
+
309
+ .progress-bar {
310
+ height: 8px;
311
+ background: var(--bg-primary);
312
+ border-radius: 4px;
313
+ overflow: hidden;
314
+ margin-top: 8px;
315
+ }
316
+
317
+ .progress-fill {
318
+ height: 100%;
319
+ background: linear-gradient(90deg, var(--accent), var(--accent-secondary));
320
+ border-radius: 4px;
321
+ transition: width 0.3s;
322
+ }
323
+
324
+ .loading-spinner {
325
+ text-align: center;
326
+ padding: 40px;
327
+ color: var(--text-secondary);
328
+ }
329
+
330
+ .error-message {
331
+ background: rgba(233, 69, 96, 0.2);
332
+ border: 1px solid var(--accent);
333
+ padding: 16px;
334
+ border-radius: 8px;
335
+ color: var(--accent);
336
+ }
337
+
338
+ .empty-state {
339
+ text-align: center;
340
+ padding: 40px;
341
+ color: var(--text-secondary);
342
+ }
343
+
344
+ .badge {
345
+ display: inline-block;
346
+ padding: 2px 8px;
347
+ border-radius: 4px;
348
+ font-size: 0.75rem;
349
+ font-weight: 600;
350
+ }
351
+
352
+ .badge-success {
353
+ background: rgba(74, 222, 128, 0.2);
354
+ color: var(--success);
355
+ }
356
+
357
+ .badge-warning {
358
+ background: rgba(251, 191, 36, 0.2);
359
+ color: var(--warning);
360
+ }
361
+ </style>
362
+ </head>
363
+ <body>
364
+ <div class="container">
365
+ <header>
366
+ <div class="logo">
367
+ <span class="logo-icon">🧠</span>
368
+ <span>Code Memory Dashboard</span>
369
+ </div>
370
+ <button class="refresh-btn" onclick="refreshData()">
371
+ <span id="refresh-icon">🔄</span>
372
+ <span>Refresh</span>
373
+ </button>
374
+ </header>
375
+
376
+ <div id="stats-container" class="stats-grid">
377
+ <div class="stat-card">
378
+ <div class="stat-value" id="stat-events">-</div>
379
+ <div class="stat-label">Total Events</div>
380
+ </div>
381
+ <div class="stat-card">
382
+ <div class="stat-value" id="stat-sessions">-</div>
383
+ <div class="stat-label">Sessions</div>
384
+ </div>
385
+ <div class="stat-card">
386
+ <div class="stat-value" id="stat-shared">-</div>
387
+ <div class="stat-label">Shared Entries</div>
388
+ </div>
389
+ <div class="stat-card">
390
+ <div class="stat-value" id="stat-vectors">-</div>
391
+ <div class="stat-label">Vectors</div>
392
+ </div>
393
+ </div>
394
+
395
+ <div class="search-container">
396
+ <input
397
+ type="text"
398
+ class="search-input"
399
+ placeholder="🔍 Search memories..."
400
+ id="search-input"
401
+ onkeyup="handleSearch(event)"
402
+ >
403
+ </div>
404
+
405
+ <div id="search-results" style="display: none; margin-bottom: 30px;">
406
+ <div class="section">
407
+ <h2 class="section-title">🔍 Search Results</h2>
408
+ <div id="search-results-content"></div>
409
+ </div>
410
+ </div>
411
+
412
+ <div class="main-grid">
413
+ <div class="section">
414
+ <h2 class="section-title">📋 Recent Sessions</h2>
415
+ <ul class="session-list" id="session-list">
416
+ <li class="loading-spinner">Loading...</li>
417
+ </ul>
418
+ </div>
419
+
420
+ <div class="section">
421
+ <h2 class="section-title">🌐 Shared Knowledge</h2>
422
+ <div id="shared-stats">
423
+ <div class="shared-item">
424
+ <div class="shared-type">
425
+ <span class="shared-icon">🔧</span>
426
+ <span>Troubleshooting</span>
427
+ </div>
428
+ <span class="shared-count" id="shared-troubleshooting">0</span>
429
+ </div>
430
+ <div class="shared-item">
431
+ <div class="shared-type">
432
+ <span class="shared-icon">✨</span>
433
+ <span>Best Practices</span>
434
+ </div>
435
+ <span class="shared-count" id="shared-best-practices">0</span>
436
+ </div>
437
+ <div class="shared-item">
438
+ <div class="shared-type">
439
+ <span class="shared-icon">⚠️</span>
440
+ <span>Common Errors</span>
441
+ </div>
442
+ <span class="shared-count" id="shared-errors">0</span>
443
+ </div>
444
+ </div>
445
+
446
+ <h3 class="section-title" style="margin-top: 24px;">♾️ Endless Mode</h3>
447
+ <div id="endless-status">
448
+ <div class="endless-status">
449
+ <div class="status-indicator inactive" id="endless-indicator"></div>
450
+ <span id="endless-mode-text">Loading...</span>
451
+ </div>
452
+ <div id="endless-details" style="display: none;">
453
+ <div style="margin-bottom: 12px;">
454
+ <span style="color: var(--text-secondary);">Continuity Score</span>
455
+ <div class="progress-bar">
456
+ <div class="progress-fill" id="continuity-bar" style="width: 0%"></div>
457
+ </div>
458
+ </div>
459
+ <div style="display: flex; justify-content: space-between; color: var(--text-secondary); font-size: 0.85rem;">
460
+ <span>Working Set: <strong id="working-set-size">0</strong></span>
461
+ <span>Consolidated: <strong id="consolidated-count">0</strong></span>
462
+ </div>
463
+ </div>
464
+ </div>
465
+ </div>
466
+ </div>
467
+
468
+ <div class="timeline-container section" style="margin-top: 30px;">
469
+ <h2 class="section-title">📊 Activity Timeline (Last 7 Days)</h2>
470
+ <div class="timeline-bar" id="timeline-bar">
471
+ <!-- Filled by JS -->
472
+ </div>
473
+ <div class="timeline-labels" id="timeline-labels">
474
+ <!-- Filled by JS -->
475
+ </div>
476
+ </div>
477
+ </div>
478
+
479
+ <script>
480
+ const API_BASE = '/api';
481
+ let refreshInterval = null;
482
+
483
+ async function fetchStats() {
484
+ try {
485
+ const response = await fetch(`${API_BASE}/stats`);
486
+ if (!response.ok) throw new Error('Failed to fetch stats');
487
+ return await response.json();
488
+ } catch (error) {
489
+ console.error('Stats fetch error:', error);
490
+ return null;
491
+ }
492
+ }
493
+
494
+ async function fetchSharedStats() {
495
+ try {
496
+ const response = await fetch(`${API_BASE}/stats/shared`);
497
+ if (!response.ok) throw new Error('Failed to fetch shared stats');
498
+ return await response.json();
499
+ } catch (error) {
500
+ console.error('Shared stats fetch error:', error);
501
+ return null;
502
+ }
503
+ }
504
+
505
+ async function fetchEndlessStatus() {
506
+ try {
507
+ const response = await fetch(`${API_BASE}/stats/endless`);
508
+ if (!response.ok) throw new Error('Failed to fetch endless status');
509
+ return await response.json();
510
+ } catch (error) {
511
+ console.error('Endless status fetch error:', error);
512
+ return null;
513
+ }
514
+ }
515
+
516
+ async function fetchSessions() {
517
+ try {
518
+ const response = await fetch(`${API_BASE}/sessions?limit=10`);
519
+ if (!response.ok) throw new Error('Failed to fetch sessions');
520
+ return await response.json();
521
+ } catch (error) {
522
+ console.error('Sessions fetch error:', error);
523
+ return null;
524
+ }
525
+ }
526
+
527
+ async function fetchTimeline() {
528
+ try {
529
+ const response = await fetch(`${API_BASE}/stats/timeline?days=7`);
530
+ if (!response.ok) throw new Error('Failed to fetch timeline');
531
+ return await response.json();
532
+ } catch (error) {
533
+ console.error('Timeline fetch error:', error);
534
+ return null;
535
+ }
536
+ }
537
+
538
+ async function searchMemories(query) {
539
+ try {
540
+ const response = await fetch(`${API_BASE}/search?q=${encodeURIComponent(query)}&limit=10`);
541
+ if (!response.ok) throw new Error('Failed to search');
542
+ return await response.json();
543
+ } catch (error) {
544
+ console.error('Search error:', error);
545
+ return null;
546
+ }
547
+ }
548
+
549
+ function formatNumber(num) {
550
+ if (num >= 1000) {
551
+ return (num / 1000).toFixed(1) + 'K';
552
+ }
553
+ return num.toString();
554
+ }
555
+
556
+ function formatTimeAgo(dateStr) {
557
+ const date = new Date(dateStr);
558
+ const now = new Date();
559
+ const diffMs = now - date;
560
+ const diffMins = Math.floor(diffMs / 60000);
561
+ const diffHours = Math.floor(diffMs / 3600000);
562
+ const diffDays = Math.floor(diffMs / 86400000);
563
+
564
+ if (diffMins < 1) return 'Just now';
565
+ if (diffMins < 60) return `${diffMins} min ago`;
566
+ if (diffHours < 24) return `${diffHours} hr ago`;
567
+ return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
568
+ }
569
+
570
+ function updateStats(stats) {
571
+ if (!stats) return;
572
+
573
+ document.getElementById('stat-events').textContent = formatNumber(stats.storage?.eventCount || 0);
574
+ document.getElementById('stat-sessions').textContent = formatNumber(stats.sessions?.total || 0);
575
+ document.getElementById('stat-vectors').textContent = formatNumber(stats.storage?.vectorCount || 0);
576
+ }
577
+
578
+ function updateSharedStats(stats) {
579
+ if (!stats) {
580
+ document.getElementById('stat-shared').textContent = '0';
581
+ return;
582
+ }
583
+
584
+ const total = (stats.troubleshooting || 0) + (stats.bestPractices || 0) + (stats.commonErrors || 0);
585
+ document.getElementById('stat-shared').textContent = formatNumber(total);
586
+ document.getElementById('shared-troubleshooting').textContent = stats.troubleshooting || 0;
587
+ document.getElementById('shared-best-practices').textContent = stats.bestPractices || 0;
588
+ document.getElementById('shared-errors').textContent = stats.commonErrors || 0;
589
+ }
590
+
591
+ function updateEndlessStatus(status) {
592
+ const indicator = document.getElementById('endless-indicator');
593
+ const text = document.getElementById('endless-mode-text');
594
+ const details = document.getElementById('endless-details');
595
+
596
+ if (!status) {
597
+ text.textContent = 'Unable to load';
598
+ return;
599
+ }
600
+
601
+ if (status.mode === 'endless') {
602
+ indicator.className = 'status-indicator active';
603
+ text.textContent = 'Endless Mode Active';
604
+ details.style.display = 'block';
605
+
606
+ const continuityPercent = (status.continuityScore || 0) * 100;
607
+ document.getElementById('continuity-bar').style.width = `${continuityPercent}%`;
608
+ document.getElementById('working-set-size').textContent = status.workingSetSize || 0;
609
+ document.getElementById('consolidated-count').textContent = status.consolidatedCount || 0;
610
+ } else {
611
+ indicator.className = 'status-indicator inactive';
612
+ text.textContent = 'Session Mode (Endless Mode Disabled)';
613
+ details.style.display = 'none';
614
+ }
615
+ }
616
+
617
+ function updateSessions(sessions) {
618
+ const container = document.getElementById('session-list');
619
+
620
+ if (!sessions || sessions.length === 0) {
621
+ container.innerHTML = '<li class="empty-state">No sessions found</li>';
622
+ return;
623
+ }
624
+
625
+ container.innerHTML = sessions.map(session => `
626
+ <li class="session-item">
627
+ <div class="session-header">
628
+ <span class="session-id">${session.sessionId.slice(0, 16)}...</span>
629
+ <span class="session-time">${formatTimeAgo(session.lastActivity || session.startTime)}</span>
630
+ </div>
631
+ <div class="session-meta">
632
+ <span>📝 ${session.eventCount || 0} events</span>
633
+ <span>👤 ${session.promptCount || 0} prompts</span>
634
+ <span>🤖 ${session.responseCount || 0} responses</span>
635
+ </div>
636
+ </li>
637
+ `).join('');
638
+ }
639
+
640
+ function updateTimeline(timeline) {
641
+ const bar = document.getElementById('timeline-bar');
642
+ const labels = document.getElementById('timeline-labels');
643
+
644
+ if (!timeline || !timeline.daily || timeline.daily.length === 0) {
645
+ bar.innerHTML = '<div class="empty-state">No activity data</div>';
646
+ labels.innerHTML = '';
647
+ return;
648
+ }
649
+
650
+ const maxTotal = Math.max(...timeline.daily.map(d => d.total), 1);
651
+
652
+ bar.innerHTML = timeline.daily.map(day => {
653
+ const height = Math.max(4, (day.total / maxTotal) * 100);
654
+ const date = new Date(day.date).toLocaleDateString('en-US', { weekday: 'short' });
655
+ return `
656
+ <div
657
+ class="timeline-day"
658
+ style="height: ${height}%"
659
+ data-tooltip="${date}: ${day.total} events"
660
+ ></div>
661
+ `;
662
+ }).join('');
663
+
664
+ const firstDate = new Date(timeline.daily[0].date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
665
+ const lastDate = new Date(timeline.daily[timeline.daily.length - 1].date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
666
+ labels.innerHTML = `<span>${firstDate}</span><span>${lastDate}</span>`;
667
+ }
668
+
669
+ function showSearchResults(results) {
670
+ const container = document.getElementById('search-results');
671
+ const content = document.getElementById('search-results-content');
672
+
673
+ if (!results || results.length === 0) {
674
+ content.innerHTML = '<div class="empty-state">No results found</div>';
675
+ container.style.display = 'block';
676
+ return;
677
+ }
678
+
679
+ content.innerHTML = results.map(result => `
680
+ <div class="session-item">
681
+ <div class="session-header">
682
+ <span class="session-id">${result.eventType || 'memory'}</span>
683
+ <span class="session-time">Score: ${(result.score * 100).toFixed(0)}%</span>
684
+ </div>
685
+ <p style="color: var(--text-secondary); margin-top: 8px;">
686
+ ${result.content?.slice(0, 200) || 'No content'}${result.content?.length > 200 ? '...' : ''}
687
+ </p>
688
+ </div>
689
+ `).join('');
690
+
691
+ container.style.display = 'block';
692
+ }
693
+
694
+ let searchTimeout = null;
695
+ function handleSearch(event) {
696
+ const query = event.target.value.trim();
697
+
698
+ if (searchTimeout) clearTimeout(searchTimeout);
699
+
700
+ if (!query) {
701
+ document.getElementById('search-results').style.display = 'none';
702
+ return;
703
+ }
704
+
705
+ searchTimeout = setTimeout(async () => {
706
+ const results = await searchMemories(query);
707
+ showSearchResults(results?.results || results || []);
708
+ }, 300);
709
+ }
710
+
711
+ async function refreshData() {
712
+ const btn = document.querySelector('.refresh-btn');
713
+ btn.classList.add('loading');
714
+ document.getElementById('refresh-icon').textContent = '⏳';
715
+
716
+ try {
717
+ const [stats, sharedStats, endlessStatus, sessions, timeline] = await Promise.all([
718
+ fetchStats(),
719
+ fetchSharedStats(),
720
+ fetchEndlessStatus(),
721
+ fetchSessions(),
722
+ fetchTimeline()
723
+ ]);
724
+
725
+ updateStats(stats);
726
+ updateSharedStats(sharedStats);
727
+ updateEndlessStatus(endlessStatus);
728
+ updateSessions(sessions?.sessions || sessions || []);
729
+ updateTimeline(timeline);
730
+ } catch (error) {
731
+ console.error('Refresh error:', error);
732
+ } finally {
733
+ btn.classList.remove('loading');
734
+ document.getElementById('refresh-icon').textContent = '🔄';
735
+ }
736
+ }
737
+
738
+ // Initial load
739
+ refreshData();
740
+
741
+ // Auto-refresh every 30 seconds
742
+ refreshInterval = setInterval(refreshData, 30000);
743
+ </script>
744
+ </body>
745
+ </html>