mailcatcher-ng 1.3.1 → 1.4.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.
@@ -4,6 +4,10 @@
4
4
  <title>Server Information - MailCatcher</title>
5
5
  <base href="<%= settings.prefix.chomp("/") %>/">
6
6
  <link href="favicon.ico" rel="icon">
7
+ <!-- Tippy.js v6 for session ID tooltips -->
8
+ <script src="https://unpkg.com/@popperjs/core@2"></script>
9
+ <script src="https://unpkg.com/tippy.js@6"></script>
10
+ <link rel="stylesheet" href="https://unpkg.com/tippy.js@6/themes/light.css">
7
11
  <style>
8
12
  * {
9
13
  margin: 0;
@@ -16,37 +20,57 @@
16
20
  background: #f5f5f5;
17
21
  color: #1a1a1a;
18
22
  min-height: 100vh;
23
+ display: block;
24
+ padding: 0;
25
+ }
26
+
27
+ .page-container {
28
+ display: grid;
29
+ grid-template-columns: 450px 1fr;
30
+ gap: 32px;
31
+ max-width: 1600px;
32
+ margin: 0 auto;
33
+ padding: 40px;
34
+ min-height: 100vh;
35
+ }
36
+
37
+ .left-column {
19
38
  display: flex;
20
- align-items: center;
21
- justify-content: center;
22
- padding: 20px;
39
+ flex-direction: column;
40
+ gap: 24px;
23
41
  }
24
42
 
25
- .container {
43
+ .right-column {
44
+ display: flex;
45
+ flex-direction: column;
46
+ gap: 24px;
47
+ }
48
+
49
+ .info-block {
26
50
  background: white;
27
51
  border-radius: 8px;
28
- padding: 40px;
52
+ padding: 32px;
29
53
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
30
- max-width: 700px;
31
- width: 100%;
32
54
  }
33
55
 
34
56
  h1 {
35
57
  font-size: 24px;
36
58
  font-weight: 700;
37
- margin: 0 0 10px 0;
59
+ margin: 0 0 4px 0;
60
+ color: #1a1a1a;
61
+ }
62
+
63
+ h2 {
64
+ font-size: 18px;
65
+ font-weight: 600;
66
+ margin: 0;
38
67
  color: #1a1a1a;
39
68
  }
40
69
 
41
70
  .page-subtitle {
42
71
  font-size: 13px;
43
72
  color: #666;
44
- margin-bottom: 32px;
45
- margin-top: 4px;
46
- }
47
-
48
- .info-section {
49
- margin-bottom: 32px;
73
+ margin-bottom: 24px;
50
74
  }
51
75
 
52
76
  .section-title {
@@ -63,12 +87,12 @@
63
87
  .info-grid {
64
88
  display: grid;
65
89
  grid-template-columns: 1fr 1fr;
66
- gap: 16px;
67
- margin-bottom: 16px;
90
+ gap: 12px;
91
+ margin-bottom: 0;
68
92
  }
69
93
 
70
94
  .info-item {
71
- padding: 12px 14px;
95
+ padding: 10px 12px;
72
96
  background: #f9f9f9;
73
97
  border-left: 3px solid #2196F3;
74
98
  border-radius: 4px;
@@ -79,26 +103,44 @@
79
103
  }
80
104
 
81
105
  .info-label {
82
- font-size: 11px;
106
+ font-size: 10px;
83
107
  font-weight: 600;
84
108
  color: #666;
85
109
  text-transform: uppercase;
86
110
  letter-spacing: 0.5px;
87
111
  display: block;
88
- margin-bottom: 6px;
112
+ margin-bottom: 4px;
89
113
  }
90
114
 
91
115
  .info-value {
92
116
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Courier New', monospace;
93
- font-size: 14px;
117
+ font-size: 13px;
94
118
  color: #1a1a1a;
95
119
  word-break: break-all;
96
120
  }
97
121
 
122
+ .settings-placeholder {
123
+ text-align: center;
124
+ padding: 48px 32px;
125
+ background: #fafafa;
126
+ border: 2px dashed #e8eaed;
127
+ border-radius: 8px;
128
+ }
129
+
130
+ .settings-placeholder .section-title {
131
+ border-bottom: none;
132
+ margin-bottom: 12px;
133
+ }
134
+
135
+ .coming-soon {
136
+ font-size: 13px;
137
+ color: #999;
138
+ margin: 0;
139
+ }
140
+
98
141
  .button-group {
99
142
  display: flex;
100
143
  gap: 12px;
101
- margin-top: 32px;
102
144
  flex-wrap: wrap;
103
145
  }
104
146
 
@@ -149,104 +191,550 @@
149
191
  .version-info {
150
192
  font-size: 12px;
151
193
  color: #999;
152
- margin-top: 24px;
153
- padding-top: 16px;
154
- border-top: 1px solid #e8eaed;
155
194
  text-align: center;
156
195
  }
196
+
197
+ /* Logs panel styles */
198
+ .logs-panel {
199
+ background: white;
200
+ border-radius: 8px;
201
+ padding: 32px;
202
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: 16px;
206
+ flex: 1;
207
+ min-height: 600px;
208
+ }
209
+
210
+ .log-search-box {
211
+ position: relative;
212
+ }
213
+
214
+ .log-search-box input {
215
+ width: 100%;
216
+ padding: 10px 36px 10px 12px;
217
+ border: 1px solid #e0e0e0;
218
+ border-radius: 6px;
219
+ font-size: 13px;
220
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto';
221
+ background: #ffffff;
222
+ }
223
+
224
+ .log-search-box input:focus {
225
+ outline: none;
226
+ border-color: #2196F3;
227
+ box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
228
+ }
229
+
230
+ .log-search-clear {
231
+ position: absolute;
232
+ right: 10px;
233
+ top: 50%;
234
+ transform: translateY(-50%);
235
+ background: none;
236
+ border: none;
237
+ color: #999;
238
+ cursor: pointer;
239
+ display: none;
240
+ font-size: 18px;
241
+ padding: 0;
242
+ width: 20px;
243
+ height: 20px;
244
+ }
245
+
246
+ .log-search-clear:hover {
247
+ color: #666;
248
+ }
249
+
250
+ .log-container {
251
+ background: #f9f9f9;
252
+ border: 1px solid #e8eaed;
253
+ border-radius: 6px;
254
+ padding: 16px;
255
+ max-height: 600px;
256
+ overflow-y: auto;
257
+ font-family: 'Monaco', 'Courier New', monospace;
258
+ flex: 1;
259
+ }
260
+
261
+ .log-entry {
262
+ display: flex;
263
+ gap: 12px;
264
+ margin: 6px 0;
265
+ padding: 4px 0;
266
+ border-bottom: 1px solid #f0f0f0;
267
+ font-size: 12px;
268
+ align-items: flex-start;
269
+ }
270
+
271
+ .log-entry:last-child {
272
+ border-bottom: none;
273
+ }
274
+
275
+ .log-entry.hidden {
276
+ display: none;
277
+ }
278
+
279
+ .log-meta {
280
+ display: flex;
281
+ gap: 8px;
282
+ flex-shrink: 0;
283
+ align-items: center;
284
+ }
285
+
286
+ .log-time {
287
+ color: #999;
288
+ min-width: 100px;
289
+ font-size: 11px;
290
+ }
291
+
292
+ .log-session {
293
+ color: #999;
294
+ font-size: 10px;
295
+ max-width: 150px;
296
+ overflow: hidden;
297
+ text-overflow: ellipsis;
298
+ white-space: nowrap;
299
+ cursor: help;
300
+ border-bottom: 1px dotted #ddd;
301
+ }
302
+
303
+ .log-type {
304
+ min-width: 80px;
305
+ flex-shrink: 0;
306
+ font-size: 11px;
307
+ font-weight: 600;
308
+ text-transform: uppercase;
309
+ }
310
+
311
+ .log-type.connection { color: #9c27b0; }
312
+ .log-type.command { color: #2196F3; }
313
+ .log-type.response { color: #34a853; }
314
+ .log-type.tls { color: #ff9800; }
315
+ .log-type.data { color: #607d8b; }
316
+ .log-type.error { color: #f44336; }
317
+
318
+ .log-direction {
319
+ min-width: 60px;
320
+ flex-shrink: 0;
321
+ font-size: 11px;
322
+ color: #666;
323
+ }
324
+
325
+ .log-direction.client::before {
326
+ content: '→ ';
327
+ color: #2196F3;
328
+ }
329
+
330
+ .log-direction.server::before {
331
+ content: '← ';
332
+ color: #34a853;
333
+ }
334
+
335
+ .log-message {
336
+ flex: 1;
337
+ color: #1a1a1a;
338
+ font-size: 12px;
339
+ word-break: break-word;
340
+ white-space: pre-wrap;
341
+ }
342
+
343
+ .log-message.error {
344
+ color: #f44336;
345
+ font-weight: 500;
346
+ }
347
+
348
+ .no-logs {
349
+ text-align: center;
350
+ padding: 40px 20px;
351
+ color: #999;
352
+ font-size: 14px;
353
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto';
354
+ }
355
+
356
+ /* Session ID tooltip styles */
357
+ .session-tooltip-content {
358
+ display: flex;
359
+ align-items: center;
360
+ gap: 8px;
361
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', monospace;
362
+ font-size: 12px;
363
+ padding: 0;
364
+ }
365
+
366
+ .session-id-value {
367
+ background: #f5f5f5;
368
+ padding: 6px 8px;
369
+ border-radius: 4px;
370
+ font-family: 'Monaco', 'Courier New', monospace;
371
+ color: #1a1a1a;
372
+ user-select: all;
373
+ }
374
+
375
+ .copy-session-btn {
376
+ background: none;
377
+ border: none;
378
+ cursor: pointer;
379
+ padding: 4px;
380
+ display: flex;
381
+ align-items: center;
382
+ color: #2196F3;
383
+ transition: all 0.2s;
384
+ }
385
+
386
+ .copy-session-btn:hover {
387
+ color: #1976D2;
388
+ }
389
+
390
+ .copy-session-btn svg {
391
+ width: 14px;
392
+ height: 14px;
393
+ }
394
+
395
+ .copy-session-btn.copied {
396
+ color: #34a853;
397
+ }
157
398
  </style>
158
399
  </head>
159
400
  <body>
160
- <div class="container">
161
- <h1>Server Information</h1>
162
- <div class="page-subtitle">Configuration and connection details</div>
401
+ <div class="page-container">
402
+ <!-- LEFT COLUMN -->
403
+ <div class="left-column">
404
+ <div class="info-block">
405
+ <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px;">
406
+ <div>
407
+ <h1>Server Information</h1>
408
+ <div class="page-subtitle">Configuration</div>
409
+ </div>
410
+ <a href="<%= settings.prefix.chomp("/") %>/" class="btn btn-primary" style="white-space: nowrap; margin-left: 16px;">
411
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
412
+ <line x1="19" y1="12" x2="5" y2="12"></line>
413
+ <polyline points="12 19 5 12 12 5"></polyline>
414
+ </svg>
415
+ Back
416
+ </a>
417
+ </div>
418
+
419
+ <div class="section-title">Network</div>
420
+ <div class="info-grid">
421
+ <div class="info-item">
422
+ <span class="info-label">Hostname</span>
423
+ <div class="info-value"><%= @hostname %></div>
424
+ </div>
425
+
426
+ <div class="info-item">
427
+ <span class="info-label">FQDN</span>
428
+ <div class="info-value"><%= @fqdn %></div>
429
+ </div>
430
+ </div>
163
431
 
164
- <div class="info-section">
165
- <div class="section-title">Network Configuration</div>
432
+ <div class="section-title" style="margin-top: 16px;">SMTP Server</div>
433
+ <div class="info-grid">
434
+ <div class="info-item">
435
+ <span class="info-label">IP Address</span>
436
+ <div class="info-value"><%= @smtp_ip %></div>
437
+ </div>
438
+
439
+ <div class="info-item">
440
+ <span class="info-label">Port</span>
441
+ <div class="info-value"><%= @smtp_port %></div>
442
+ </div>
443
+ </div>
166
444
 
167
- <div class="info-item full">
168
- <span class="info-label">Hostname</span>
169
- <div class="info-value"><%= @hostname %></div>
445
+ <div class="section-title" style="margin-top: 16px;">HTTP Server</div>
446
+ <div class="info-grid">
447
+ <div class="info-item">
448
+ <span class="info-label">IP Address</span>
449
+ <div class="info-value"><%= @http_ip %></div>
450
+ </div>
451
+
452
+ <div class="info-item">
453
+ <span class="info-label">Port</span>
454
+ <div class="info-value"><%= @http_port %></div>
455
+ </div>
456
+
457
+ <div class="info-item full">
458
+ <span class="info-label">Base Path</span>
459
+ <div class="info-value"><%= @http_path %></div>
460
+ </div>
461
+ </div>
170
462
  </div>
171
463
 
172
- <div class="info-item full">
173
- <span class="info-label">FQDN (Fully Qualified Domain Name)</span>
174
- <div class="info-value"><%= @fqdn %></div>
464
+ <div class="info-block settings-placeholder">
465
+ <div class="section-title">Server Settings</div>
466
+ <p class="coming-soon">Coming soon</p>
467
+ <div class="button-group" style="margin-top: 16px; justify-content: center;">
468
+ <a href="<%= File.join(settings.prefix.chomp("/"), "websocket-test") %>" class="btn btn-secondary">
469
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
470
+ <circle cx="12" cy="12" r="1"></circle>
471
+ <path d="M12 1v6m0 6v4"></path>
472
+ <path d="M4.22 4.22l4.24 4.24m5.08 5.08l4.24 4.24"></path>
473
+ <path d="M1 12h6m6 0h4"></path>
474
+ <path d="M4.22 19.78l4.24-4.24m5.08-5.08l4.24-4.24"></path>
475
+ </svg>
476
+ Diagnostics
477
+ </a>
478
+ </div>
479
+ <div class="version-info" style="margin-top: 12px;">
480
+ MailCatcher v<%= @version %>
481
+ </div>
175
482
  </div>
176
483
  </div>
177
484
 
178
- <div class="info-section">
179
- <div class="section-title">Active Connections</div>
485
+ <!-- RIGHT COLUMN -->
486
+ <div class="right-column">
487
+ <div class="logs-panel">
488
+ <div style="display: flex; justify-content: space-between; align-items: center;">
489
+ <h2>Server Logs</h2>
490
+ <button id="autoRefreshBtn" class="btn btn-secondary" style="padding: 8px 12px; font-size: 12px;">
491
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;">
492
+ <path d="M21.5 2v6h-6"></path>
493
+ <path d="M2.5 22v-6h6"></path>
494
+ <path d="M2 11.5a10 10 0 0 1 18.8-4.3"></path>
495
+ <path d="M22 12.5a10 10 0 0 1-18.8 4.2"></path>
496
+ </svg>
497
+ Auto-refresh: ON
498
+ </button>
499
+ </div>
180
500
 
181
- <div class="info-grid">
182
- <div class="info-item">
183
- <span class="info-label">HTTP Connections</span>
184
- <div class="info-value"><%= @http_connections %></div>
501
+ <div class="log-search-box">
502
+ <input type="text" id="logSearch" placeholder="Filter logs..." />
503
+ <button class="log-search-clear" id="logSearchClear">×</button>
185
504
  </div>
186
505
 
187
- <div class="info-item">
188
- <span class="info-label">SMTP Connections</span>
189
- <div class="info-value"><%= @smtp_connections %></div>
506
+ <div class="log-container" id="logContainer">
507
+ <div class="no-logs">
508
+ Loading logs...
509
+ </div>
190
510
  </div>
191
511
  </div>
192
512
  </div>
513
+ </div>
193
514
 
194
- <div class="info-section">
195
- <div class="section-title">SMTP Server</div>
196
-
197
- <div class="info-item">
198
- <span class="info-label">IP Address</span>
199
- <div class="info-value"><%= @smtp_ip %></div>
200
- </div>
515
+ <script>
516
+ // Real-time log refresh system
517
+ var logContainer = document.getElementById('logContainer');
518
+ var searchInput = document.getElementById('logSearch');
519
+ var searchClear = document.getElementById('logSearchClear');
520
+ var autoRefreshBtn = document.getElementById('autoRefreshBtn');
521
+
522
+ var autoRefreshEnabled = true;
523
+ var refreshInterval = null;
524
+ var lastLogCount = 0;
525
+ var allLogEntries = [];
526
+
527
+ // Fetch logs from server
528
+ function fetchLogs() {
529
+ fetch('<%= settings.prefix.chomp("/") %>/logs.json')
530
+ .then(function(response) {
531
+ return response.json();
532
+ })
533
+ .then(function(data) {
534
+ allLogEntries = data.entries || [];
535
+ renderLogs();
536
+ })
537
+ .catch(function(error) {
538
+ console.error('Error fetching logs:', error);
539
+ });
540
+ }
201
541
 
202
- <div class="info-item">
203
- <span class="info-label">Port</span>
204
- <div class="info-value"><%= @smtp_port %></div>
205
- </div>
206
- </div>
542
+ // Render logs with current search filter applied
543
+ function renderLogs() {
544
+ var query = searchInput.value.toLowerCase().trim();
545
+
546
+ logContainer.innerHTML = '';
547
+
548
+ if (allLogEntries.length === 0) {
549
+ logContainer.innerHTML = '<div class="no-logs">No server logs available</div>';
550
+ return;
551
+ }
552
+
553
+ allLogEntries.forEach(function(entry) {
554
+ var searchable = JSON.stringify(entry).toLowerCase();
555
+ var shouldShow = !query || searchable.indexOf(query) >= 0;
556
+
557
+ var timeStr = '';
558
+ try {
559
+ var time = new Date(entry.timestamp);
560
+ var hours = String(time.getHours()).padStart(2, '0');
561
+ var minutes = String(time.getMinutes()).padStart(2, '0');
562
+ var seconds = String(time.getSeconds()).padStart(2, '0');
563
+ var ms = String(time.getMilliseconds()).padStart(3, '0');
564
+ timeStr = hours + ':' + minutes + ':' + seconds + '.' + ms;
565
+ } catch (e) {
566
+ timeStr = '??:??:??';
567
+ }
568
+
569
+ var sessionId = entry.session_id || '';
570
+ var sessionDisplay = sessionId.substring(0, 8) + '...';
571
+
572
+ var entryDiv = document.createElement('div');
573
+ entryDiv.className = 'log-entry' + (shouldShow ? '' : ' hidden');
574
+ entryDiv.setAttribute('data-searchable', searchable);
575
+ entryDiv.innerHTML =
576
+ '<div class="log-meta">' +
577
+ '<div class="log-time">' + timeStr + '</div>' +
578
+ '<div class="log-session" data-session-id="' + escapeHtml(sessionId) + '">' + sessionDisplay + '</div>' +
579
+ '</div>' +
580
+ '<div class="log-type ' + entry.type + '">' + entry.type + '</div>' +
581
+ '<div class="log-direction ' + entry.direction + '">' + entry.direction + '</div>' +
582
+ '<div class="log-message' + (entry.type === 'error' ? ' error' : '') + '">' +
583
+ escapeHtml(entry.message) +
584
+ '</div>';
585
+
586
+ logContainer.appendChild(entryDiv);
587
+ });
588
+
589
+ // Initialize Tippy tooltips for session IDs
590
+ initSessionTooltips();
591
+
592
+ // Auto-scroll to bottom
593
+ logContainer.scrollTop = logContainer.scrollHeight;
594
+ }
207
595
 
208
- <div class="info-section">
209
- <div class="section-title">HTTP Server</div>
596
+ // HTML escape utility
597
+ function escapeHtml(text) {
598
+ var map = {
599
+ '&': '&amp;',
600
+ '<': '&lt;',
601
+ '>': '&gt;',
602
+ '"': '&quot;',
603
+ "'": '&#039;'
604
+ };
605
+ return text.replace(/[&<>"']/g, function(m) { return map[m]; });
606
+ }
210
607
 
211
- <div class="info-item">
212
- <span class="info-label">IP Address</span>
213
- <div class="info-value"><%= @http_ip %></div>
214
- </div>
608
+ // Filter logs based on search input
609
+ function filterEntries() {
610
+ renderLogs();
611
+ searchClear.style.display = searchInput.value ? 'block' : 'none';
612
+ }
215
613
 
216
- <div class="info-item">
217
- <span class="info-label">Port</span>
218
- <div class="info-value"><%= @http_port %></div>
219
- </div>
614
+ // Toggle auto-refresh
615
+ function toggleAutoRefresh() {
616
+ autoRefreshEnabled = !autoRefreshEnabled;
617
+
618
+ if (autoRefreshEnabled) {
619
+ autoRefreshBtn.textContent = '';
620
+ autoRefreshBtn.innerHTML =
621
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;">' +
622
+ '<path d="M21.5 2v6h-6"></path>' +
623
+ '<path d="M2.5 22v-6h6"></path>' +
624
+ '<path d="M2 11.5a10 10 0 0 1 18.8-4.3"></path>' +
625
+ '<path d="M22 12.5a10 10 0 0 1-18.8 4.2"></path>' +
626
+ '</svg> Auto-refresh: ON';
627
+
628
+ // Fetch any missed logs immediately
629
+ fetchLogs();
630
+
631
+ // Resume auto-refresh interval
632
+ refreshInterval = setInterval(function() {
633
+ fetchLogs();
634
+ }, 1000);
635
+ } else {
636
+ autoRefreshBtn.textContent = '';
637
+ autoRefreshBtn.innerHTML =
638
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;">' +
639
+ '<path d="M21.5 2v6h-6"></path>' +
640
+ '<path d="M2.5 22v-6h6"></path>' +
641
+ '<path d="M2 11.5a10 10 0 0 1 18.8-4.3"></path>' +
642
+ '<path d="M22 12.5a10 10 0 0 1-18.8 4.2"></path>' +
643
+ '</svg> Auto-refresh: OFF';
644
+
645
+ // Stop auto-refresh interval
646
+ if (refreshInterval) {
647
+ clearInterval(refreshInterval);
648
+ refreshInterval = null;
649
+ }
650
+ }
651
+ }
220
652
 
221
- <div class="info-item full">
222
- <span class="info-label">Base Path</span>
223
- <div class="info-value"><%= @http_path %></div>
224
- </div>
225
- </div>
653
+ // Initialize Tippy tooltips for session IDs
654
+ function initSessionTooltips() {
655
+ var sessionElements = document.querySelectorAll('.log-session[data-session-id]');
656
+ sessionElements.forEach(function(element) {
657
+ // Destroy existing tooltip if it exists
658
+ if (element._tippy) {
659
+ element._tippy.destroy();
660
+ }
661
+
662
+ var sessionId = element.getAttribute('data-session-id');
663
+
664
+ // Create tooltip content
665
+ var tooltipDiv = document.createElement('div');
666
+ tooltipDiv.className = 'session-tooltip-content';
667
+ tooltipDiv.innerHTML =
668
+ '<div class="session-id-value">' + escapeHtml(sessionId) + '</div>' +
669
+ '<button class="copy-session-btn" data-session-id="' + escapeHtml(sessionId) + '">' +
670
+ '<svg class="copy-icon" data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
671
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"></path>' +
672
+ '</svg>' +
673
+ '<svg class="checkmark-icon" style="display: none;" data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
674
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75"></path>' +
675
+ '</svg>' +
676
+ '</button>';
677
+
678
+ // Create Tippy instance
679
+ tippy(element, {
680
+ content: tooltipDiv,
681
+ theme: 'light',
682
+ placement: 'top',
683
+ interactive: true,
684
+ onShow: function(instance) {
685
+ // Attach click handler when tooltip shows
686
+ var copyBtn = tooltipDiv.querySelector('.copy-session-btn');
687
+ if (copyBtn && !copyBtn._clickHandlerAttached) {
688
+ copyBtn._clickHandlerAttached = true;
689
+ copyBtn.addEventListener('click', function(e) {
690
+ e.preventDefault();
691
+ var sessionIdToCopy = copyBtn.getAttribute('data-session-id');
692
+ copyToClipboard(sessionIdToCopy, copyBtn);
693
+ });
694
+ }
695
+ }
696
+ });
697
+ });
698
+ }
226
699
 
227
- <div class="button-group">
228
- <a href="<%= settings.prefix.chomp("/") %>/" class="btn btn-primary">
229
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
230
- <line x1="19" y1="12" x2="5" y2="12"></line>
231
- <polyline points="12 19 5 12 12 5"></polyline>
232
- </svg>
233
- Back to Inbox
234
- </a>
235
- <a href="<%= File.join(settings.prefix.chomp("/"), "websocket-test") %>" class="btn btn-secondary">
236
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
237
- <circle cx="12" cy="12" r="1"></circle>
238
- <path d="M12 1v6m0 6v4"></path>
239
- <path d="M4.22 4.22l4.24 4.24m5.08 5.08l4.24 4.24"></path>
240
- <path d="M1 12h6m6 0h4"></path>
241
- <path d="M4.22 19.78l4.24-4.24m5.08-5.08l4.24-4.24"></path>
242
- </svg>
243
- Diagnostics
244
- </a>
245
- </div>
700
+ // Copy session ID to clipboard
701
+ function copyToClipboard(text, button) {
702
+ navigator.clipboard.writeText(text).then(function() {
703
+ var copyIcon = button.querySelector('.copy-icon');
704
+ var checkmarkIcon = button.querySelector('.checkmark-icon');
705
+
706
+ // Show checkmark
707
+ if (copyIcon) copyIcon.style.display = 'none';
708
+ if (checkmarkIcon) checkmarkIcon.style.display = 'block';
709
+ button.classList.add('copied');
710
+
711
+ // Reset after 2 seconds
712
+ setTimeout(function() {
713
+ if (copyIcon) copyIcon.style.display = 'block';
714
+ if (checkmarkIcon) checkmarkIcon.style.display = 'none';
715
+ button.classList.remove('copied');
716
+ }, 2000);
717
+ }).catch(function(err) {
718
+ console.error('Failed to copy:', err);
719
+ });
720
+ }
246
721
 
247
- <div class="version-info">
248
- MailCatcher v<%= @version %>
249
- </div>
250
- </div>
722
+ // Event listeners
723
+ searchInput.addEventListener('keyup', filterEntries);
724
+ searchClear.addEventListener('click', function() {
725
+ searchInput.value = '';
726
+ filterEntries();
727
+ searchInput.focus();
728
+ });
729
+ autoRefreshBtn.addEventListener('click', toggleAutoRefresh);
730
+
731
+ // Initial load and setup auto-refresh
732
+ fetchLogs();
733
+ refreshInterval = setInterval(function() {
734
+ if (autoRefreshEnabled) {
735
+ fetchLogs();
736
+ }
737
+ }, 1000); // Fetch every 1 second
738
+ </script>
251
739
  </body>
252
740
  </html>