mailcatcher-ng 1.3.2 → 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.
- checksums.yaml +4 -4
- data/lib/mail_catcher/mail.rb +42 -7
- data/lib/mail_catcher/smtp.rb +8 -6
- data/lib/mail_catcher/version.rb +1 -1
- data/lib/mail_catcher/web/application.rb +30 -0
- data/lib/mail_catcher.rb +5 -0
- data/views/server_info.erb +582 -94
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6539793c4e31686b97ba0502cc4c48c47c49ace16230fea4cf2f2cb8d06704c7
|
|
4
|
+
data.tar.gz: d74ed846c8205f2b37b9b433b618424929d12515a928d23b4a481fe03a5993d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd19f4d57d9710adc6c60a21aeeefbfb2623e57d6725463f5bd01b3ce5e5fcb93360d1543301bc9655b64fc814994d036e3c8cf8de3940643f69ccf1f4063297
|
|
7
|
+
data.tar.gz: 7aed22f4e9a54ca7e239f7401d95931cfce90f309d2beae5d28049aa939b4903bb2d256aac8ff694c3740bf62835d6bf2c5f90d8c9b53fd3f855a2b282e6860e
|
data/lib/mail_catcher/mail.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "eventmachine"
|
|
4
|
+
require "fileutils"
|
|
4
5
|
require "json"
|
|
5
6
|
require "mail"
|
|
6
7
|
require "sqlite3"
|
|
@@ -8,9 +9,10 @@ require "sqlite3"
|
|
|
8
9
|
module MailCatcher::Mail extend self
|
|
9
10
|
def db
|
|
10
11
|
@__db ||= begin
|
|
11
|
-
|
|
12
|
+
db_path = determine_db_path
|
|
13
|
+
SQLite3::Database.new(db_path, :type_translation => true).tap do |db|
|
|
12
14
|
db.execute(<<-SQL)
|
|
13
|
-
CREATE TABLE message (
|
|
15
|
+
CREATE TABLE IF NOT EXISTS message (
|
|
14
16
|
id INTEGER PRIMARY KEY ASC,
|
|
15
17
|
sender TEXT,
|
|
16
18
|
recipients TEXT,
|
|
@@ -22,7 +24,7 @@ module MailCatcher::Mail extend self
|
|
|
22
24
|
)
|
|
23
25
|
SQL
|
|
24
26
|
db.execute(<<-SQL)
|
|
25
|
-
CREATE TABLE message_part (
|
|
27
|
+
CREATE TABLE IF NOT EXISTS message_part (
|
|
26
28
|
id INTEGER PRIMARY KEY ASC,
|
|
27
29
|
message_id INTEGER NOT NULL,
|
|
28
30
|
cid TEXT,
|
|
@@ -37,7 +39,7 @@ module MailCatcher::Mail extend self
|
|
|
37
39
|
)
|
|
38
40
|
SQL
|
|
39
41
|
db.execute(<<-SQL)
|
|
40
|
-
CREATE TABLE smtp_transcript (
|
|
42
|
+
CREATE TABLE IF NOT EXISTS smtp_transcript (
|
|
41
43
|
id INTEGER PRIMARY KEY ASC,
|
|
42
44
|
message_id INTEGER,
|
|
43
45
|
session_id TEXT NOT NULL,
|
|
@@ -55,13 +57,25 @@ module MailCatcher::Mail extend self
|
|
|
55
57
|
FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE
|
|
56
58
|
)
|
|
57
59
|
SQL
|
|
58
|
-
db.execute("CREATE INDEX idx_smtp_transcript_message_id ON smtp_transcript(message_id)")
|
|
59
|
-
db.execute("CREATE INDEX idx_smtp_transcript_session_id ON smtp_transcript(session_id)")
|
|
60
|
+
db.execute("CREATE INDEX IF NOT EXISTS idx_smtp_transcript_message_id ON smtp_transcript(message_id)")
|
|
61
|
+
db.execute("CREATE INDEX IF NOT EXISTS idx_smtp_transcript_session_id ON smtp_transcript(session_id)")
|
|
60
62
|
db.execute("PRAGMA foreign_keys = ON")
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
end
|
|
64
66
|
|
|
67
|
+
def determine_db_path
|
|
68
|
+
if MailCatcher.options && MailCatcher.options[:persistence]
|
|
69
|
+
# Use a persistent SQLite file in the user's home directory
|
|
70
|
+
db_dir = File.expand_path('~/.mailcatcher')
|
|
71
|
+
FileUtils.mkdir_p(db_dir) unless Dir.exist?(db_dir)
|
|
72
|
+
File.join(db_dir, 'mailcatcher.db')
|
|
73
|
+
else
|
|
74
|
+
# Use in-memory database
|
|
75
|
+
':memory:'
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
65
79
|
def add_message(message)
|
|
66
80
|
@add_message_query ||= db.prepare("INSERT INTO message (sender, recipients, subject, source, type, size, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))")
|
|
67
81
|
|
|
@@ -311,7 +325,10 @@ module MailCatcher::Mail extend self
|
|
|
311
325
|
|
|
312
326
|
def delete!
|
|
313
327
|
@delete_all_messages_query ||= db.prepare "DELETE FROM message"
|
|
328
|
+
@delete_all_transcripts_query ||= db.prepare "DELETE FROM smtp_transcript"
|
|
329
|
+
|
|
314
330
|
@delete_all_messages_query.execute
|
|
331
|
+
@delete_all_transcripts_query.execute
|
|
315
332
|
|
|
316
333
|
EventMachine.next_tick do
|
|
317
334
|
MailCatcher::Bus.push(type: "clear")
|
|
@@ -438,7 +455,7 @@ module MailCatcher::Mail extend self
|
|
|
438
455
|
|
|
439
456
|
def message_transcript(message_id)
|
|
440
457
|
@message_transcript_query ||= db.prepare(<<-SQL)
|
|
441
|
-
SELECT id, session_id, client_ip, client_port,
|
|
458
|
+
SELECT id, message_id, session_id, client_ip, client_port,
|
|
442
459
|
server_ip, server_port, tls_enabled, tls_protocol,
|
|
443
460
|
tls_cipher, connection_started_at, connection_ended_at,
|
|
444
461
|
entries, created_at
|
|
@@ -470,6 +487,24 @@ module MailCatcher::Mail extend self
|
|
|
470
487
|
end
|
|
471
488
|
end
|
|
472
489
|
|
|
490
|
+
def all_transcript_entries
|
|
491
|
+
@all_transcript_entries_query ||= db.prepare(<<-SQL)
|
|
492
|
+
SELECT id, message_id, session_id, client_ip, client_port,
|
|
493
|
+
server_ip, server_port, tls_enabled, tls_protocol,
|
|
494
|
+
tls_cipher, connection_started_at, connection_ended_at,
|
|
495
|
+
entries, created_at
|
|
496
|
+
FROM smtp_transcript
|
|
497
|
+
ORDER BY created_at DESC
|
|
498
|
+
SQL
|
|
499
|
+
|
|
500
|
+
@all_transcript_entries_query.execute.map do |row|
|
|
501
|
+
result = Hash[@all_transcript_entries_query.columns.zip(row)]
|
|
502
|
+
result['entries'] = JSON.parse(result['entries']) if result['entries']
|
|
503
|
+
result['tls_enabled'] = result['tls_enabled'] == 1
|
|
504
|
+
result
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
473
508
|
private
|
|
474
509
|
|
|
475
510
|
def parse_authentication_results(auth_header)
|
data/lib/mail_catcher/smtp.rb
CHANGED
|
@@ -18,6 +18,7 @@ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
|
|
18
18
|
@session_id = SecureRandom.uuid
|
|
19
19
|
@connection_started_at = Time.now
|
|
20
20
|
@data_started = false
|
|
21
|
+
@last_message_id = nil
|
|
21
22
|
super
|
|
22
23
|
end
|
|
23
24
|
|
|
@@ -58,8 +59,9 @@ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
|
|
58
59
|
@connection_ended_at = Time.now
|
|
59
60
|
log_transcript('connection', 'server', "Connection closed")
|
|
60
61
|
|
|
61
|
-
# Save transcript
|
|
62
|
-
|
|
62
|
+
# Save transcript with the last message if available, otherwise without message_id
|
|
63
|
+
# This ensures "Connection closed" is included in the transcript
|
|
64
|
+
save_transcript(@last_message_id) if @transcript_entries.any?
|
|
63
65
|
|
|
64
66
|
super
|
|
65
67
|
end
|
|
@@ -205,6 +207,7 @@ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
|
|
205
207
|
|
|
206
208
|
begin
|
|
207
209
|
message_id = MailCatcher::Mail.add_message current_message
|
|
210
|
+
@last_message_id = message_id
|
|
208
211
|
rescue => e
|
|
209
212
|
$stderr.puts "Error in add_message: #{e.message}"
|
|
210
213
|
$stderr.puts e.backtrace.join("\n")
|
|
@@ -213,16 +216,15 @@ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
|
|
213
216
|
|
|
214
217
|
MailCatcher::Mail.delete_older_messages!
|
|
215
218
|
|
|
216
|
-
#
|
|
217
|
-
|
|
219
|
+
# Don't save transcript here - save it when connection closes (in unbind)
|
|
220
|
+
# This ensures "Connection closed" entry is included
|
|
218
221
|
|
|
219
222
|
true
|
|
220
223
|
rescue => exception
|
|
221
224
|
log_transcript('error', 'server', "Exception: #{exception.class} - #{exception.message}")
|
|
222
225
|
MailCatcher.log_exception("Error receiving message", @current_message, exception)
|
|
223
226
|
|
|
224
|
-
#
|
|
225
|
-
save_transcript(nil)
|
|
227
|
+
# Don't save transcript here - save it when connection closes (in unbind)
|
|
226
228
|
|
|
227
229
|
false
|
|
228
230
|
ensure
|
data/lib/mail_catcher/version.rb
CHANGED
|
@@ -101,9 +101,39 @@ module MailCatcher
|
|
|
101
101
|
@fqdn = fqdn
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# Fetch all SMTP transcripts and flatten entries for display
|
|
105
|
+
transcripts = Mail.all_transcript_entries
|
|
106
|
+
@log_entries = transcripts.flat_map do |transcript|
|
|
107
|
+
transcript['entries'].map do |entry|
|
|
108
|
+
entry.merge({
|
|
109
|
+
'session_id' => transcript['session_id'],
|
|
110
|
+
'client_ip' => transcript['client_ip'],
|
|
111
|
+
'server_port' => transcript['server_port'],
|
|
112
|
+
'tls_enabled' => transcript['tls_enabled']
|
|
113
|
+
})
|
|
114
|
+
end
|
|
115
|
+
end.sort_by { |e| e['timestamp'] }
|
|
116
|
+
|
|
104
117
|
erb :server_info
|
|
105
118
|
end
|
|
106
119
|
|
|
120
|
+
get "/logs.json" do
|
|
121
|
+
content_type :json
|
|
122
|
+
transcripts = Mail.all_transcript_entries
|
|
123
|
+
log_entries = transcripts.flat_map do |transcript|
|
|
124
|
+
transcript['entries'].map do |entry|
|
|
125
|
+
entry.merge({
|
|
126
|
+
'session_id' => transcript['session_id'],
|
|
127
|
+
'client_ip' => transcript['client_ip'],
|
|
128
|
+
'server_port' => transcript['server_port'],
|
|
129
|
+
'tls_enabled' => transcript['tls_enabled']
|
|
130
|
+
})
|
|
131
|
+
end
|
|
132
|
+
end.sort_by { |e| e['timestamp'] }
|
|
133
|
+
|
|
134
|
+
JSON.generate(entries: log_entries)
|
|
135
|
+
end
|
|
136
|
+
|
|
107
137
|
delete "/" do
|
|
108
138
|
if MailCatcher.quittable?
|
|
109
139
|
MailCatcher.quit!
|
data/lib/mail_catcher.rb
CHANGED
|
@@ -85,6 +85,7 @@ module MailCatcher
|
|
|
85
85
|
http_port: '1080',
|
|
86
86
|
http_path: '/',
|
|
87
87
|
messages_limit: nil,
|
|
88
|
+
persistence: false,
|
|
88
89
|
verbose: false,
|
|
89
90
|
daemon: !windows?,
|
|
90
91
|
browse: false,
|
|
@@ -157,6 +158,10 @@ module MailCatcher
|
|
|
157
158
|
options[:messages_limit] = count
|
|
158
159
|
end
|
|
159
160
|
|
|
161
|
+
parser.on('--persistence', 'Store messages in a persistent SQLite database file') do
|
|
162
|
+
options[:persistence] = true
|
|
163
|
+
end
|
|
164
|
+
|
|
160
165
|
parser.on('--http-path PATH', String, 'Add a prefix to all HTTP paths') do |path|
|
|
161
166
|
clean_path = Rack::Utils.clean_path_info("/#{path}")
|
|
162
167
|
|
data/views/server_info.erb
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
padding: 20px;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
gap: 24px;
|
|
23
41
|
}
|
|
24
42
|
|
|
25
|
-
.
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
67
|
-
margin-bottom:
|
|
90
|
+
gap: 12px;
|
|
91
|
+
margin-bottom: 0;
|
|
68
92
|
}
|
|
69
93
|
|
|
70
94
|
.info-item {
|
|
71
|
-
padding: 12px
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
162
|
-
<div class="
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
<
|
|
169
|
-
|
|
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-
|
|
173
|
-
<
|
|
174
|
-
<
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
<
|
|
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="
|
|
188
|
-
<
|
|
189
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
596
|
+
// HTML escape utility
|
|
597
|
+
function escapeHtml(text) {
|
|
598
|
+
var map = {
|
|
599
|
+
'&': '&',
|
|
600
|
+
'<': '<',
|
|
601
|
+
'>': '>',
|
|
602
|
+
'"': '"',
|
|
603
|
+
"'": '''
|
|
604
|
+
};
|
|
605
|
+
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
|
606
|
+
}
|
|
210
607
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
608
|
+
// Filter logs based on search input
|
|
609
|
+
function filterEntries() {
|
|
610
|
+
renderLogs();
|
|
611
|
+
searchClear.style.display = searchInput.value ? 'block' : 'none';
|
|
612
|
+
}
|
|
215
613
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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>
|