trackguard 0.20.0 → 0.21.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e13b68029bc80d015c4f69531d17e5b362092d6df2d720dfb7c1563b5b4e24c
4
- data.tar.gz: e676a88de14a04d1c42c0a78f7e895a7ef5011c7567e93a168e82e8f950bfeb1
3
+ metadata.gz: 7c250ded678f9970940e87fff8a0d46ff58f8e504a0f63b1abcdd3a2a7955ec7
4
+ data.tar.gz: 33d162a61e298288facf7f02424864b8f0588cd7f6cfce437cb866f6c82944e8
5
5
  SHA512:
6
- metadata.gz: d4074d349ac565380d3365ef21ca99e61836601f51fe5822cff34a3184f93cea71dc0ab541b2a08f2f0ccb49fade36c89a97f8681a5609748088ecdb661f6f84
7
- data.tar.gz: 1783d02fa8caedd5816efce4cca6faab7c305af260549de8ece369c22ff47b279725e9503dc5c3d8cade1f5069516faaee594fd362e16b92939317168f4d7dfc
6
+ metadata.gz: 6fcf99a57cbc641734aa16923597924c5cd795074592e3f63d4f8c747c25f2182cf9b2a170d5fdc2ad4ee6913bed7a56e40c65ba39d2bbfa2b47fb338915db96
7
+ data.tar.gz: c7e9dab2b98f2b5c0d790714a9fbffd81fb3e7028559fe6acebdb7f1946eae7108c41aa847fbbb431270a344516714eea35c5aa4b41cf598db29c90e76b0a480
@@ -7,8 +7,8 @@
7
7
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", sans-serif;
8
8
  font-size: 16px;
9
9
  line-height: 1.5;
10
- background-color: #0f0909;
11
- color: #e0e0e0;
10
+ background-color: #f3f4f6;
11
+ color: #111827;
12
12
  -webkit-font-smoothing: antialiased;
13
13
  min-height: 100vh;
14
14
  }
@@ -22,8 +22,8 @@
22
22
 
23
23
  /* ── Header ────────────────────────────────────────────────────────── */
24
24
  .tg-header {
25
- background: #190e0e;
26
- border-bottom: 1px solid #2e1515;
25
+ background: #0d0f12;
26
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
27
27
  padding: 0.875rem 0;
28
28
  }
29
29
 
@@ -58,7 +58,7 @@
58
58
  align-items: center;
59
59
  gap: 0.3rem;
60
60
  font-size: 0.8125rem;
61
- color: #666666;
61
+ color: #6b7280;
62
62
  text-decoration: none;
63
63
  transition: color 0.15s;
64
64
  white-space: nowrap;
@@ -71,13 +71,13 @@
71
71
  }
72
72
 
73
73
  .tg-back-link:hover {
74
- color: #e0e0e0;
74
+ color: #f3f4f6;
75
75
  }
76
76
 
77
77
  /* ── Nav ───────────────────────────────────────────────────────────── */
78
78
  .tg-nav {
79
- background: #190e0e;
80
- border-bottom: 1px solid #2e1515;
79
+ background: #ffffff;
80
+ border-bottom: 1px solid #e5e7eb;
81
81
  }
82
82
 
83
83
  .tg-nav > .tg-container {
@@ -88,16 +88,16 @@
88
88
  display: inline-block;
89
89
  padding: 0.5rem 1rem;
90
90
  font-size: 0.8125rem;
91
- color: #666666;
91
+ color: #6b7280;
92
92
  text-decoration: none;
93
93
  border-bottom: 2px solid transparent;
94
94
  transition: color 0.15s;
95
95
  }
96
96
 
97
- .tg-nav__link:hover { color: #e0e0e0; }
97
+ .tg-nav__link:hover { color: #111827; }
98
98
 
99
99
  .tg-nav__link--active {
100
- color: #f87171;
100
+ color: #e53e3e;
101
101
  border-bottom-color: #e53e3e;
102
102
  }
103
103
 
@@ -116,7 +116,7 @@
116
116
  font-size: 1.25rem;
117
117
  font-weight: 700;
118
118
  letter-spacing: -0.02em;
119
- color: #f0f0f0;
119
+ color: #111827;
120
120
  }
121
121
 
122
122
  /* ── Stats ─────────────────────────────────────────────────────────── */
@@ -128,10 +128,11 @@
128
128
  }
129
129
 
130
130
  .tg-stat {
131
- background: #190e0e;
132
- border: 1px solid #2e1515;
133
- border-radius: 8px;
131
+ background: #ffffff;
132
+ border: 1px solid #e5e7eb;
133
+ border-radius: 12px;
134
134
  padding: 1.25rem;
135
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
135
136
  }
136
137
 
137
138
  .tg-stat__label {
@@ -139,14 +140,14 @@
139
140
  font-weight: 500;
140
141
  text-transform: uppercase;
141
142
  letter-spacing: 0.08em;
142
- color: #666666;
143
+ color: #9ca3af;
143
144
  margin: 0 0 0.25rem;
144
145
  }
145
146
 
146
147
  .tg-stat__value {
147
148
  font-size: 1.625rem;
148
149
  font-weight: 700;
149
- color: #f0f0f0;
150
+ color: #111827;
150
151
  margin: 0;
151
152
  }
152
153
 
@@ -154,7 +155,7 @@
154
155
  .tg-page-title__count {
155
156
  font-size: 0.9375rem;
156
157
  font-weight: 400;
157
- color: #666666;
158
+ color: #9ca3af;
158
159
  margin-left: 0.5rem;
159
160
  }
160
161
 
@@ -167,12 +168,13 @@
167
168
  }
168
169
 
169
170
  .tg-panel {
170
- background: #190e0e;
171
- border: 1px solid #2e1515;
172
- border-radius: 8px;
171
+ background: #ffffff;
172
+ border: 1px solid #e5e7eb;
173
+ border-radius: 12px;
173
174
  padding: 1rem;
174
175
  margin-bottom: 1rem;
175
176
  overflow: hidden;
177
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
176
178
  }
177
179
 
178
180
  .tg-panel__heading {
@@ -180,13 +182,13 @@
180
182
  font-weight: 600;
181
183
  text-transform: uppercase;
182
184
  letter-spacing: 0.08em;
183
- color: #f0f0f0;
185
+ color: #111827;
184
186
  margin: 0 0 0.75rem;
185
187
  }
186
188
 
187
189
  .tg-panel__sub {
188
190
  font-weight: 400;
189
- color: #666666;
191
+ color: #9ca3af;
190
192
  text-transform: none;
191
193
  letter-spacing: 0;
192
194
  }
@@ -209,17 +211,19 @@
209
211
  padding: 0.25rem 0.625rem;
210
212
  border-radius: 4px;
211
213
  font-size: 0.8125rem;
212
- color: #e0e0e0;
214
+ color: #374151;
213
215
  text-decoration: none;
214
- background: #2e1515;
216
+ background: #ffffff;
217
+ border: 1px solid #e5e7eb;
215
218
  }
216
219
 
217
- .tg-pagination__link:hover { background: #3d1c1c; }
220
+ .tg-pagination__link:hover { background: #f3f4f6; }
218
221
 
219
222
  .tg-pagination__link--active {
220
223
  background: #e53e3e;
221
224
  color: #ffffff;
222
225
  font-weight: 600;
226
+ border-color: #e53e3e;
223
227
  }
224
228
 
225
229
  .tg-pagination__link--disabled {
@@ -230,7 +234,7 @@
230
234
  .tg-pagination__ellipsis {
231
235
  padding: 0.25rem 0.25rem;
232
236
  font-size: 0.8125rem;
233
- color: #666666;
237
+ color: #9ca3af;
234
238
  }
235
239
 
236
240
  /* ── Table ─────────────────────────────────────────────────────────── */
@@ -248,22 +252,22 @@
248
252
  font-weight: 500;
249
253
  text-transform: uppercase;
250
254
  letter-spacing: 0.08em;
251
- color: #666666;
252
- border-bottom: 1px solid #2e1515;
255
+ color: #9ca3af;
256
+ border-bottom: 1px solid #e5e7eb;
253
257
  }
254
258
 
255
259
  .tg-th--right { text-align: right; }
256
260
 
257
261
  .tg-td {
258
262
  padding: 0.75rem 0.875rem;
259
- border-bottom: 1px solid #2e1515;
260
- color: #f0f0f0;
263
+ border-bottom: 1px solid #f3f4f6;
264
+ color: #111827;
261
265
  font-weight: 500;
262
266
  }
263
267
 
264
268
  .tg-td--num {
265
269
  text-align: right;
266
- color: #e0e0e0;
270
+ color: #9ca3af;
267
271
  font-weight: 400;
268
272
  }
269
273
 
@@ -271,24 +275,24 @@
271
275
 
272
276
  .tg-td--bare {
273
277
  padding: 0;
274
- border-bottom: 1px solid #2e1515;
278
+ border-bottom: 1px solid #e5e7eb;
275
279
  }
276
280
 
277
281
  /* ── Empty state ───────────────────────────────────────────────────── */
278
282
  .tg-empty {
279
283
  font-size: 0.9375rem;
280
- color: #666666;
284
+ color: #9ca3af;
281
285
  padding: 1rem 0 2rem;
282
286
  margin: 0;
283
287
  }
284
288
 
285
289
  /* ── Accordion row ─────────────────────────────────────────────────── */
286
290
  .tg-row--flagged {
287
- background: rgba(127, 29, 29, 0.15);
291
+ background: rgba(239, 68, 68, 0.05);
288
292
  }
289
293
 
290
294
  .tg-row--whitelisted {
291
- background: rgba(6, 78, 59, 0.15);
295
+ background: rgba(34, 197, 94, 0.05);
292
296
  }
293
297
 
294
298
  .tg-summary {
@@ -308,7 +312,7 @@
308
312
  overflow: hidden;
309
313
  text-overflow: ellipsis;
310
314
  white-space: nowrap;
311
- color: #f0f0f0;
315
+ color: #111827;
312
316
  font-weight: 500;
313
317
  font-size: 0.875rem;
314
318
  }
@@ -316,16 +320,27 @@
316
320
  .tg-summary__ip {
317
321
  font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
318
322
  font-size: 0.75rem;
319
- color: #e0e0e0;
323
+ color: #6b7280;
320
324
  width: 8.5rem;
321
325
  flex-shrink: 0;
322
326
  }
323
327
 
328
+ .tg-summary__trace {
329
+ font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
330
+ font-size: 0.75rem;
331
+ color: #9ca3af;
332
+ width: 7rem;
333
+ flex-shrink: 0;
334
+ overflow: hidden;
335
+ text-overflow: ellipsis;
336
+ white-space: nowrap;
337
+ }
338
+
324
339
  .tg-summary__flag {
325
340
  width: 1.5rem;
326
341
  text-align: center;
327
342
  flex-shrink: 0;
328
- color: #666666;
343
+ color: #d1d5db;
329
344
  font-size: 0.875rem;
330
345
  }
331
346
 
@@ -333,13 +348,13 @@
333
348
  width: 1.5rem;
334
349
  text-align: center;
335
350
  flex-shrink: 0;
336
- color: #666666;
351
+ color: #d1d5db;
337
352
  font-size: 0.875rem;
338
353
  }
339
354
 
340
355
  .tg-summary__time {
341
356
  font-size: 0.8125rem;
342
- color: #e0e0e0;
357
+ color: #9ca3af;
343
358
  width: 7.5rem;
344
359
  flex-shrink: 0;
345
360
  }
@@ -361,7 +376,7 @@
361
376
  /* ── Detail panel ──────────────────────────────────────────────────── */
362
377
  .tg-detail {
363
378
  padding: 0 1rem 1rem 1rem;
364
- background: #190e0e;
379
+ background: #f9fafb;
365
380
  }
366
381
 
367
382
  .tg-detail__grid {
@@ -376,7 +391,7 @@
376
391
  font-weight: 500;
377
392
  text-transform: uppercase;
378
393
  letter-spacing: 0.08em;
379
- color: #666666;
394
+ color: #9ca3af;
380
395
  margin: 0 0 0.5rem;
381
396
  }
382
397
 
@@ -394,13 +409,13 @@
394
409
  }
395
410
 
396
411
  .tg-dl__term {
397
- color: #666666;
412
+ color: #9ca3af;
398
413
  width: 6rem;
399
414
  flex-shrink: 0;
400
415
  }
401
416
 
402
417
  .tg-dl__def {
403
- color: #e0e0e0;
418
+ color: #374151;
404
419
  margin: 0;
405
420
  }
406
421
 
@@ -411,11 +426,11 @@
411
426
 
412
427
  .tg-dl__def--break { word-break: break-all; }
413
428
 
414
- .tg-dl__def--flagged { color: #fca5a5; }
429
+ .tg-dl__def--flagged { color: #dc2626; }
415
430
 
416
- .tg-dl__def--muted { color: #666666; }
431
+ .tg-dl__def--muted { color: #9ca3af; }
417
432
 
418
- .tg-dl__def--whitelisted { color: #6ee7b7; }
433
+ .tg-dl__def--whitelisted { color: #059669; }
419
434
 
420
435
  /* ── Flag actions ──────────────────────────────────────────────────── */
421
436
  .tg-detail__actions {
@@ -423,7 +438,7 @@
423
438
  align-items: center;
424
439
  gap: 0.5rem;
425
440
  padding: 0.75rem 0 0;
426
- border-top: 1px solid #2e1515;
441
+ border-top: 1px solid #e5e7eb;
427
442
  margin-top: 0.75rem;
428
443
  }
429
444
 
@@ -436,36 +451,37 @@
436
451
 
437
452
  .tg-input {
438
453
  flex: 1;
439
- background: #0f0909;
440
- border: 1px solid #2e1515;
454
+ background: #ffffff;
455
+ border: 1px solid #d1d5db;
441
456
  border-radius: 4px;
442
- color: #e0e0e0;
457
+ color: #374151;
443
458
  font-size: 0.8125rem;
444
459
  padding: 0.375rem 0.625rem;
445
460
  outline: none;
446
461
  }
447
462
 
448
463
  .tg-input:focus { border-color: #e53e3e; }
449
- .tg-input::placeholder { color: #444; }
464
+ .tg-input::placeholder { color: #9ca3af; }
450
465
 
451
466
  .tg-btn {
452
- border: none;
467
+ border: 1px solid transparent;
453
468
  border-radius: 4px;
454
469
  cursor: pointer;
455
470
  font-size: 0.8125rem;
456
471
  font-weight: 500;
457
472
  padding: 0.375rem 0.75rem;
458
473
  white-space: nowrap;
474
+ background: transparent;
459
475
  }
460
476
 
461
- .tg-btn--danger { background: #7f1d1d; color: #fca5a5; }
462
- .tg-btn--danger:hover { background: #991b1b; }
477
+ .tg-btn--danger { border-color: #dc2626; color: #dc2626; }
478
+ .tg-btn--danger:hover { background: rgba(220, 38, 38, 0.06); }
463
479
 
464
- .tg-btn--ghost { background: #2e1515; color: #e0e0e0; }
465
- .tg-btn--ghost:hover { background: #3d1c1c; }
480
+ .tg-btn--ghost { border-color: #d1d5db; color: #374151; background: #ffffff; }
481
+ .tg-btn--ghost:hover { background: #f3f4f6; }
466
482
 
467
- .tg-btn--whitelist { background: #064e3b; color: #6ee7b7; }
468
- .tg-btn--whitelist:hover { background: #065f46; }
483
+ .tg-btn--whitelist { border-color: #059669; color: #059669; }
484
+ .tg-btn--whitelist:hover { background: rgba(5, 150, 105, 0.06); }
469
485
 
470
486
  /* ── Responsive ────────────────────────────────────────────────────── */
471
487
  @media (max-width: 900px) {
@@ -0,0 +1,133 @@
1
+ <% visitor = pv.visitor %>
2
+ <% flagged = visitor&.flagged_at.present? %>
3
+ <% whitelisted = visitor&.whitelisted_ip&.active? %>
4
+ <% row_classes = [ ("tg-row--flagged" if flagged), ("tg-row--whitelisted" if whitelisted) ].compact.join(" ") %>
5
+ <tr class="<%= row_classes %>">
6
+ <td class="tg-td--bare">
7
+ <details>
8
+ <summary class="tg-summary">
9
+ <span class="tg-summary__ip"><%= visitor&.ip || "—" %></span>
10
+ <span class="tg-summary__trace"><%= pv.trace_id.presence || "—" %></span>
11
+ <span class="tg-summary__path"><%= pv.path %></span>
12
+ <span class="tg-summary__flag">
13
+ <% if flagged %>
14
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="tg-flag-icon" title="Flagged">
15
+ <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
16
+ </svg>
17
+ <% else %>
18
+
19
+ <% end %>
20
+ </span>
21
+ <span class="tg-summary__whitelist">
22
+ <% if whitelisted %>
23
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="tg-whitelist-icon" title="Whitelisted">
24
+ <path fill-rule="evenodd" d="M9.661 2.237a.531.531 0 01.678 0 11.947 11.947 0 007.078 2.749.5.5 0 01.479.425c.069.52.104 1.05.104 1.589 0 5.162-3.26 9.563-7.834 11.256a.48.48 0 01-.332 0C5.26 16.563 2 12.162 2 7c0-.538.035-1.069.104-1.589a.5.5 0 01.48-.425 11.947 11.947 0 007.077-2.749zm2.55 5.513a.75.75 0 00-1.06-1.06L9 8.79l-.84-.84a.75.75 0 10-1.061 1.06l1.37 1.37a.75.75 0 001.06 0l2.682-2.67z" clip-rule="evenodd" />
25
+ </svg>
26
+ <% else %>
27
+
28
+ <% end %>
29
+ </span>
30
+ <span class="tg-summary__time"><%= pv.created_at.strftime("%b %-d, %H:%M") %></span>
31
+ </summary>
32
+ <div class="tg-detail">
33
+ <% if visitor %>
34
+ <div class="tg-detail__actions">
35
+ <% if whitelisted %>
36
+ <%= button_to "Remove from whitelist", unwhitelist_visitor_path,
37
+ params: { id: visitor.id },
38
+ method: :patch,
39
+ class: "tg-btn tg-btn--ghost",
40
+ data: { confirm: "Remove whitelist entry for #{visitor.ip}?" } %>
41
+ <% else %>
42
+ <%= button_to "Whitelist", whitelist_visitor_path,
43
+ params: { id: visitor.id },
44
+ method: :patch,
45
+ class: "tg-btn tg-btn--whitelist",
46
+ data: { confirm: "Whitelist #{visitor.ip} for 7 days?" } %>
47
+ <% end %>
48
+ <% if flagged %>
49
+ <%= button_to "Unflag", unflag_visitor_path, params: { id: visitor.id }, method: :patch, class: "tg-btn tg-btn--ghost" %>
50
+ <% else %>
51
+ <%= form_with url: flag_visitor_path, method: :patch, class: "tg-flag-form" do |f| %>
52
+ <%= hidden_field_tag :id, visitor.id %>
53
+ <%= f.submit "Flag", class: "tg-btn tg-btn--danger" %>
54
+ <%= f.text_field :flag_reason, placeholder: "Flag reason (optional)", class: "tg-input", autocomplete: "off" %>
55
+ <%= f.text_field :name, placeholder: "Name (optional, auto-detected if blank)", class: "tg-input", autocomplete: "off" %>
56
+ <% end %>
57
+ <% end %>
58
+ </div>
59
+ <% end %>
60
+ <div class="tg-detail__grid">
61
+ <div>
62
+ <p class="tg-detail__group-label">Visitor</p>
63
+ <div class="tg-dl">
64
+ <div class="tg-dl__row">
65
+ <span class="tg-dl__term">IP</span>
66
+ <span class="tg-dl__def tg-dl__def--mono"><%= visitor&.ip || "—" %></span>
67
+ </div>
68
+ <div class="tg-dl__row">
69
+ <span class="tg-dl__term">First seen</span>
70
+ <span class="tg-dl__def"><%= visitor&.first_seen_at&.strftime("%b %-d %Y, %H:%M") || "—" %></span>
71
+ </div>
72
+ <div class="tg-dl__row">
73
+ <span class="tg-dl__term">Last seen</span>
74
+ <span class="tg-dl__def"><%= visitor&.last_seen_at&.strftime("%b %-d %Y, %H:%M") || "—" %></span>
75
+ </div>
76
+ <div class="tg-dl__row">
77
+ <span class="tg-dl__term">User agent</span>
78
+ <span class="tg-dl__def tg-dl__def--break"><%= visitor&.user_agent.presence || "—" %></span>
79
+ </div>
80
+ <div class="tg-dl__row">
81
+ <span class="tg-dl__term">Name</span>
82
+ <span class="tg-dl__def"><%= visitor&.name.presence || "—" %></span>
83
+ </div>
84
+ <div class="tg-dl__row">
85
+ <span class="tg-dl__term">Flag status</span>
86
+ <% if flagged %>
87
+ <span class="tg-dl__def tg-dl__def--flagged">
88
+ Flagged <%= visitor.flagged_at.strftime("%b %-d %Y, %H:%M") %>
89
+ <% if visitor.flagged_by.present? %> by <strong><%= visitor.flagged_by %></strong><% end %>
90
+ <% if visitor.flag_reason.present? %> — <%= visitor.flag_reason %><% end %>
91
+ </span>
92
+ <% else %>
93
+ <span class="tg-dl__def tg-dl__def--muted">Not flagged</span>
94
+ <% end %>
95
+ </div>
96
+ <div class="tg-dl__row">
97
+ <span class="tg-dl__term">Whitelist</span>
98
+ <% if whitelisted %>
99
+ <span class="tg-dl__def tg-dl__def--whitelisted">
100
+ Active until <%= visitor.whitelisted_ip.expires_at.strftime("%b %-d %Y, %H:%M") %>
101
+ </span>
102
+ <% else %>
103
+ <span class="tg-dl__def tg-dl__def--muted">Not whitelisted</span>
104
+ <% end %>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ <div>
109
+ <p class="tg-detail__group-label">This Visit</p>
110
+ <div class="tg-dl">
111
+ <div class="tg-dl__row">
112
+ <span class="tg-dl__term">Session</span>
113
+ <span class="tg-dl__def tg-dl__def--mono"><%= pv.session_id.presence || "—" %></span>
114
+ </div>
115
+ <div class="tg-dl__row">
116
+ <span class="tg-dl__term">Trace ID</span>
117
+ <span class="tg-dl__def tg-dl__def--mono"><%= pv.trace_id.presence || "—" %></span>
118
+ </div>
119
+ <div class="tg-dl__row">
120
+ <span class="tg-dl__term">Referrer</span>
121
+ <span class="tg-dl__def tg-dl__def--break"><%= pv.referer.presence || "—" %></span>
122
+ </div>
123
+ <div class="tg-dl__row">
124
+ <span class="tg-dl__term">Source</span>
125
+ <span class="tg-dl__def"><%= pv.source.presence || "—" %></span>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </details>
132
+ </td>
133
+ </tr>
@@ -98,138 +98,7 @@
98
98
  <table class="tg-table">
99
99
  <tbody>
100
100
  <% @recent.each do |pv| %>
101
- <% visitor = pv.visitor %>
102
- <% flagged = visitor&.flagged_at.present? %>
103
- <% whitelisted = visitor&.whitelisted_ip&.active? %>
104
- <% row_classes = [ ("tg-row--flagged" if flagged), ("tg-row--whitelisted" if whitelisted) ].compact.join(" ") %>
105
- <tr class="<%= row_classes %>">
106
- <td class="tg-td--bare">
107
- <details>
108
- <summary class="tg-summary">
109
- <span class="tg-summary__path"><%= pv.path %></span>
110
- <span class="tg-summary__ip"><%= visitor&.ip || "—" %></span>
111
- <span class="tg-summary__flag">
112
- <% if flagged %>
113
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="tg-flag-icon" title="Flagged">
114
- <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
115
- </svg>
116
- <% else %>
117
-
118
- <% end %>
119
- </span>
120
- <span class="tg-summary__whitelist">
121
- <% if whitelisted %>
122
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="tg-whitelist-icon" title="Whitelisted">
123
- <path fill-rule="evenodd" d="M9.661 2.237a.531.531 0 01.678 0 11.947 11.947 0 007.078 2.749.5.5 0 01.479.425c.069.52.104 1.05.104 1.589 0 5.162-3.26 9.563-7.834 11.256a.48.48 0 01-.332 0C5.26 16.563 2 12.162 2 7c0-.538.035-1.069.104-1.589a.5.5 0 01.48-.425 11.947 11.947 0 007.077-2.749zm2.55 5.513a.75.75 0 00-1.06-1.06L9 8.79l-.84-.84a.75.75 0 10-1.061 1.06l1.37 1.37a.75.75 0 001.06 0l2.682-2.67z" clip-rule="evenodd" />
124
- </svg>
125
- <% else %>
126
-
127
- <% end %>
128
- </span>
129
- <span class="tg-summary__time"><%= pv.created_at.strftime("%b %-d, %H:%M") %></span>
130
- </summary>
131
- <div class="tg-detail">
132
- <% if visitor %>
133
- <div class="tg-detail__actions">
134
- <% if whitelisted %>
135
- <%= button_to "Remove from whitelist", unwhitelist_visitor_path,
136
- params: { id: visitor.id },
137
- method: :patch,
138
- class: "tg-btn tg-btn--ghost",
139
- data: { confirm: "Remove whitelist entry for #{visitor.ip}?" } %>
140
- <% else %>
141
- <%= button_to "Whitelist", whitelist_visitor_path,
142
- params: { id: visitor.id },
143
- method: :patch,
144
- class: "tg-btn tg-btn--whitelist",
145
- data: { confirm: "Whitelist #{visitor.ip} for 7 days?" } %>
146
- <% end %>
147
- <% if flagged %>
148
- <%= button_to "Unflag", unflag_visitor_path, params: { id: visitor.id }, method: :patch, class: "tg-btn tg-btn--ghost" %>
149
- <% else %>
150
- <%= form_with url: flag_visitor_path, method: :patch, class: "tg-flag-form" do |f| %>
151
- <%= hidden_field_tag :id, visitor.id %>
152
- <%= f.submit "Flag", class: "tg-btn tg-btn--danger" %>
153
- <%= f.text_field :flag_reason, placeholder: "Flag reason (optional)", class: "tg-input", autocomplete: "off" %>
154
- <%= f.text_field :name, placeholder: "Name (optional, auto-detected if blank)", class: "tg-input", autocomplete: "off" %>
155
- <% end %>
156
- <% end %>
157
- </div>
158
- <% end %>
159
- <div class="tg-detail__grid">
160
- <div>
161
- <p class="tg-detail__group-label">Visitor</p>
162
- <div class="tg-dl">
163
- <div class="tg-dl__row">
164
- <span class="tg-dl__term">IP</span>
165
- <span class="tg-dl__def tg-dl__def--mono"><%= visitor&.ip || "—" %></span>
166
- </div>
167
- <div class="tg-dl__row">
168
- <span class="tg-dl__term">First seen</span>
169
- <span class="tg-dl__def"><%= visitor&.first_seen_at&.strftime("%b %-d %Y, %H:%M") || "—" %></span>
170
- </div>
171
- <div class="tg-dl__row">
172
- <span class="tg-dl__term">Last seen</span>
173
- <span class="tg-dl__def"><%= visitor&.last_seen_at&.strftime("%b %-d %Y, %H:%M") || "—" %></span>
174
- </div>
175
- <div class="tg-dl__row">
176
- <span class="tg-dl__term">User agent</span>
177
- <span class="tg-dl__def tg-dl__def--break"><%= visitor&.user_agent.presence || "—" %></span>
178
- </div>
179
- <div class="tg-dl__row">
180
- <span class="tg-dl__term">Name</span>
181
- <span class="tg-dl__def"><%= visitor&.name.presence || "—" %></span>
182
- </div>
183
- <div class="tg-dl__row">
184
- <span class="tg-dl__term">Flag status</span>
185
- <% if flagged %>
186
- <span class="tg-dl__def tg-dl__def--flagged">
187
- Flagged <%= visitor.flagged_at.strftime("%b %-d %Y, %H:%M") %>
188
- <% if visitor.flagged_by.present? %> by <strong><%= visitor.flagged_by %></strong><% end %>
189
- <% if visitor.flag_reason.present? %> — <%= visitor.flag_reason %><% end %>
190
- </span>
191
- <% else %>
192
- <span class="tg-dl__def tg-dl__def--muted">Not flagged</span>
193
- <% end %>
194
- </div>
195
- <div class="tg-dl__row">
196
- <span class="tg-dl__term">Whitelist</span>
197
- <% if whitelisted %>
198
- <span class="tg-dl__def tg-dl__def--whitelisted">
199
- Active until <%= visitor.whitelisted_ip.expires_at.strftime("%b %-d %Y, %H:%M") %>
200
- </span>
201
- <% else %>
202
- <span class="tg-dl__def tg-dl__def--muted">Not whitelisted</span>
203
- <% end %>
204
- </div>
205
- </div>
206
- </div>
207
- <div>
208
- <p class="tg-detail__group-label">This Visit</p>
209
- <div class="tg-dl">
210
- <div class="tg-dl__row">
211
- <span class="tg-dl__term">Session</span>
212
- <span class="tg-dl__def tg-dl__def--mono"><%= pv.session_id.presence || "—" %></span>
213
- </div>
214
- <div class="tg-dl__row">
215
- <span class="tg-dl__term">Trace ID</span>
216
- <span class="tg-dl__def tg-dl__def--mono"><%= pv.trace_id.presence || "—" %></span>
217
- </div>
218
- <div class="tg-dl__row">
219
- <span class="tg-dl__term">Referrer</span>
220
- <span class="tg-dl__def tg-dl__def--break"><%= pv.referer.presence || "—" %></span>
221
- </div>
222
- <div class="tg-dl__row">
223
- <span class="tg-dl__term">Source</span>
224
- <span class="tg-dl__def"><%= pv.source.presence || "—" %></span>
225
- </div>
226
- </div>
227
- </div>
228
- </div>
229
- </div>
230
- </details>
231
- </td>
232
- </tr>
101
+ <%= render "trackguard/admin/visit_row", pv: pv %>
233
102
  <% end %>
234
103
  </tbody>
235
104
  </table>
@@ -10,138 +10,7 @@
10
10
  <table class="tg-table">
11
11
  <tbody>
12
12
  <% @visits.each do |pv| %>
13
- <% visitor = pv.visitor %>
14
- <% flagged = visitor&.flagged_at.present? %>
15
- <% whitelisted = visitor&.whitelisted_ip&.active? %>
16
- <% row_classes = [ ("tg-row--flagged" if flagged), ("tg-row--whitelisted" if whitelisted) ].compact.join(" ") %>
17
- <tr class="<%= row_classes %>">
18
- <td class="tg-td--bare">
19
- <details>
20
- <summary class="tg-summary">
21
- <span class="tg-summary__path"><%= pv.path %></span>
22
- <span class="tg-summary__ip"><%= visitor&.ip || "—" %></span>
23
- <span class="tg-summary__flag">
24
- <% if flagged %>
25
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="tg-flag-icon" title="Flagged">
26
- <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
27
- </svg>
28
- <% else %>
29
-
30
- <% end %>
31
- </span>
32
- <span class="tg-summary__whitelist">
33
- <% if whitelisted %>
34
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="tg-whitelist-icon" title="Whitelisted">
35
- <path fill-rule="evenodd" d="M9.661 2.237a.531.531 0 01.678 0 11.947 11.947 0 007.078 2.749.5.5 0 01.479.425c.069.52.104 1.05.104 1.589 0 5.162-3.26 9.563-7.834 11.256a.48.48 0 01-.332 0C5.26 16.563 2 12.162 2 7c0-.538.035-1.069.104-1.589a.5.5 0 01.48-.425 11.947 11.947 0 007.077-2.749zm2.55 5.513a.75.75 0 00-1.06-1.06L9 8.79l-.84-.84a.75.75 0 10-1.061 1.06l1.37 1.37a.75.75 0 001.06 0l2.682-2.67z" clip-rule="evenodd" />
36
- </svg>
37
- <% else %>
38
-
39
- <% end %>
40
- </span>
41
- <span class="tg-summary__time"><%= pv.created_at.strftime("%b %-d, %H:%M") %></span>
42
- </summary>
43
- <div class="tg-detail">
44
- <% if visitor %>
45
- <div class="tg-detail__actions">
46
- <% if whitelisted %>
47
- <%= button_to "Remove from whitelist", unwhitelist_visitor_path,
48
- params: { id: visitor.id },
49
- method: :patch,
50
- class: "tg-btn tg-btn--ghost",
51
- data: { confirm: "Remove whitelist entry for #{visitor.ip}?" } %>
52
- <% else %>
53
- <%= button_to "Whitelist", whitelist_visitor_path,
54
- params: { id: visitor.id },
55
- method: :patch,
56
- class: "tg-btn tg-btn--whitelist",
57
- data: { confirm: "Whitelist #{visitor.ip} for 7 days?" } %>
58
- <% end %>
59
- <% if flagged %>
60
- <%= button_to "Unflag", unflag_visitor_path, params: { id: visitor.id }, method: :patch, class: "tg-btn tg-btn--ghost" %>
61
- <% else %>
62
- <%= form_with url: flag_visitor_path, method: :patch, class: "tg-flag-form" do |f| %>
63
- <%= hidden_field_tag :id, visitor.id %>
64
- <%= f.submit "Flag", class: "tg-btn tg-btn--danger" %>
65
- <%= f.text_field :flag_reason, placeholder: "Flag reason (optional)", class: "tg-input", autocomplete: "off" %>
66
- <%= f.text_field :name, placeholder: "Name (optional, auto-detected if blank)", class: "tg-input", autocomplete: "off" %>
67
- <% end %>
68
- <% end %>
69
- </div>
70
- <% end %>
71
- <div class="tg-detail__grid">
72
- <div>
73
- <p class="tg-detail__group-label">Visitor</p>
74
- <div class="tg-dl">
75
- <div class="tg-dl__row">
76
- <span class="tg-dl__term">IP</span>
77
- <span class="tg-dl__def tg-dl__def--mono"><%= visitor&.ip || "—" %></span>
78
- </div>
79
- <div class="tg-dl__row">
80
- <span class="tg-dl__term">First seen</span>
81
- <span class="tg-dl__def"><%= visitor&.first_seen_at&.strftime("%b %-d %Y, %H:%M") || "—" %></span>
82
- </div>
83
- <div class="tg-dl__row">
84
- <span class="tg-dl__term">Last seen</span>
85
- <span class="tg-dl__def"><%= visitor&.last_seen_at&.strftime("%b %-d %Y, %H:%M") || "—" %></span>
86
- </div>
87
- <div class="tg-dl__row">
88
- <span class="tg-dl__term">User agent</span>
89
- <span class="tg-dl__def tg-dl__def--break"><%= visitor&.user_agent.presence || "—" %></span>
90
- </div>
91
- <div class="tg-dl__row">
92
- <span class="tg-dl__term">Name</span>
93
- <span class="tg-dl__def"><%= visitor&.name.presence || "—" %></span>
94
- </div>
95
- <div class="tg-dl__row">
96
- <span class="tg-dl__term">Flag status</span>
97
- <% if flagged %>
98
- <span class="tg-dl__def tg-dl__def--flagged">
99
- Flagged <%= visitor.flagged_at.strftime("%b %-d %Y, %H:%M") %>
100
- <% if visitor.flagged_by.present? %> by <strong><%= visitor.flagged_by %></strong><% end %>
101
- <% if visitor.flag_reason.present? %> — <%= visitor.flag_reason %><% end %>
102
- </span>
103
- <% else %>
104
- <span class="tg-dl__def tg-dl__def--muted">Not flagged</span>
105
- <% end %>
106
- </div>
107
- <div class="tg-dl__row">
108
- <span class="tg-dl__term">Whitelist</span>
109
- <% if whitelisted %>
110
- <span class="tg-dl__def tg-dl__def--whitelisted">
111
- Active until <%= visitor.whitelisted_ip.expires_at.strftime("%b %-d %Y, %H:%M") %>
112
- </span>
113
- <% else %>
114
- <span class="tg-dl__def tg-dl__def--muted">Not whitelisted</span>
115
- <% end %>
116
- </div>
117
- </div>
118
- </div>
119
- <div>
120
- <p class="tg-detail__group-label">This Visit</p>
121
- <div class="tg-dl">
122
- <div class="tg-dl__row">
123
- <span class="tg-dl__term">Session</span>
124
- <span class="tg-dl__def tg-dl__def--mono"><%= pv.session_id.presence || "—" %></span>
125
- </div>
126
- <div class="tg-dl__row">
127
- <span class="tg-dl__term">Trace ID</span>
128
- <span class="tg-dl__def tg-dl__def--mono"><%= pv.trace_id.presence || "—" %></span>
129
- </div>
130
- <div class="tg-dl__row">
131
- <span class="tg-dl__term">Referrer</span>
132
- <span class="tg-dl__def tg-dl__def--break"><%= pv.referer.presence || "—" %></span>
133
- </div>
134
- <div class="tg-dl__row">
135
- <span class="tg-dl__term">Source</span>
136
- <span class="tg-dl__def"><%= pv.source.presence || "—" %></span>
137
- </div>
138
- </div>
139
- </div>
140
- </div>
141
- </div>
142
- </details>
143
- </td>
144
- </tr>
13
+ <%= render "trackguard/admin/visit_row", pv: pv %>
145
14
  <% end %>
146
15
  </tbody>
147
16
  </table>
@@ -1,3 +1,3 @@
1
1
  module Trackguard
2
- VERSION = "0.20.0".freeze
2
+ VERSION = "0.21.0".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trackguard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Krzysztof Rygielski
@@ -70,6 +70,7 @@ files:
70
70
  - app/services/trackguard/application_service.rb
71
71
  - app/services/trackguard/page_view_recorder.rb
72
72
  - app/views/layouts/trackguard/admin.html.erb
73
+ - app/views/trackguard/admin/_visit_row.html.erb
73
74
  - app/views/trackguard/admin/dashboards/show.html.erb
74
75
  - app/views/trackguard/admin/visits/_pagination.html.erb
75
76
  - app/views/trackguard/admin/visits/index.html.erb