trackguard 0.19.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: 90c710c8d2149047c75e5b99151b708e9741e29385d17de7c4f101ad9db697cf
4
- data.tar.gz: 49aaf485a932c241d9c18986120f639305485f4c316653923e9ad9a018566e01
3
+ metadata.gz: 7c250ded678f9970940e87fff8a0d46ff58f8e504a0f63b1abcdd3a2a7955ec7
4
+ data.tar.gz: 33d162a61e298288facf7f02424864b8f0588cd7f6cfce437cb866f6c82944e8
5
5
  SHA512:
6
- metadata.gz: b1a17b99009baaacee17c4473bf33b811870df411541cee1c055807f2b8981b4ab09ba0c28eeee15adc0a8ab87081f6b463b494435618cafafb2aea424b95226
7
- data.tar.gz: 8da17d592514268ced0ab53cb0c2331671eea10702deaac46264f90874f2af33e60f952a1c89b7c1f920b6ad35ad93297bfe6ab786c796816c3704d35a17424d
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,29 @@
1
+ module Trackguard
2
+ module Admin
3
+ module Overridable
4
+ extend ActiveSupport::Concern
5
+
6
+ private
7
+
8
+ def visitor_scope
9
+ Visitor.all
10
+ end
11
+
12
+ def page_view_scope
13
+ PageView.all
14
+ end
15
+
16
+ def after_action_path
17
+ dashboard_path
18
+ end
19
+
20
+ def set_visitor
21
+ @visitor = if params[:ip].present?
22
+ visitor_scope.find_by!(ip: params[:ip])
23
+ else
24
+ visitor_scope.find(params[:id])
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,46 +1,25 @@
1
1
  module Trackguard
2
2
  module Admin
3
3
  class AnalyticsController < BaseController
4
+ include Overridable
5
+
4
6
  skip_before_action :verify_authenticity_token, if: :valid_api_token?
5
7
 
6
- # rubocop:disable Metrics/AbcSize
7
8
  def show
8
- @total_today = PageView.today.count
9
- @total_week = PageView.this_week.count
10
- @total_month = PageView.this_month.count
11
-
12
- base = time_scope
13
- @top_pages = base.group(:path).order("count_all DESC").limit(10).count
14
- @top_referrers = base.with_referrer.group(:referer).order("count_all DESC").limit(10).count
15
- @top_sources = base.with_source.group(:source).order("count_all DESC").limit(10).count
16
-
17
- @recent = visitor_filtered(
18
- time_scope(PageView.order(created_at: :desc).limit(20).includes(visitor: :whitelisted_ip))
9
+ query = AnalyticsQuery.call(
10
+ scope: page_view_scope,
11
+ time_scope: apply_time_scope(page_view_scope),
12
+ limit: 10
19
13
  )
20
14
 
21
15
  render json: {
22
- totals: { today: @total_today, week: @total_week, month: @total_month },
23
- top_pages: @top_pages,
24
- top_referrers: @top_referrers,
25
- top_sources: @top_sources,
26
- recent: @recent.map do |pv|
27
- {
28
- path: pv.path,
29
- ip: pv.visitor&.ip,
30
- flagged_at: pv.visitor.flagged_at,
31
- flagged_by: pv.visitor.flagged_by,
32
- whitelisted: pv.visitor.whitelisted_ip&.active? || false,
33
- user_agent: pv.user_agent,
34
- session_id: pv.session_id,
35
- trace_id: pv.trace_id,
36
- referer: pv.referer,
37
- source: pv.source,
38
- created_at: pv.created_at
39
- }
40
- end
16
+ totals: query.totals,
17
+ top_pages: query.top_pages,
18
+ top_referrers: query.top_referrers,
19
+ top_sources: query.top_sources,
20
+ recent: visitor_filtered(apply_time_scope(query.recent)).map { |view| serialize_page_view(view) }
41
21
  }
42
22
  end
43
- # rubocop:enable Metrics/AbcSize
44
23
 
45
24
  private
46
25
 
@@ -50,7 +29,7 @@ module Trackguard
50
29
  super
51
30
  end
52
31
 
53
- def time_scope(base = PageView.all)
32
+ def apply_time_scope(base)
54
33
  params[:since].present? ? base.where(created_at: parsed_since..) : base.last_30
55
34
  end
56
35
 
@@ -64,8 +43,8 @@ module Trackguard
64
43
 
65
44
  def visitor_filtered(scope)
66
45
  if params.key?(:flagged)
67
- visitor_scope = cast_bool(params[:flagged]) ? Visitor.flagged : Visitor.unflagged
68
- scope = scope.joins(:visitor).merge(visitor_scope)
46
+ flagged_scope = cast_bool(params[:flagged]) ? Visitor.flagged : Visitor.unflagged
47
+ scope = scope.joins(:visitor).merge(flagged_scope)
69
48
  end
70
49
  if params.key?(:whitelisted)
71
50
  scope = if cast_bool(params[:whitelisted])
@@ -80,6 +59,22 @@ module Trackguard
80
59
  def cast_bool(val)
81
60
  ActiveRecord::Type::Boolean.new.cast(val)
82
61
  end
62
+
63
+ def serialize_page_view(view)
64
+ {
65
+ path: view.path,
66
+ ip: view.visitor&.ip,
67
+ flagged_at: view.visitor.flagged_at,
68
+ flagged_by: view.visitor.flagged_by,
69
+ whitelisted: view.visitor.whitelisted_ip&.active? || false,
70
+ user_agent: view.user_agent,
71
+ session_id: view.session_id,
72
+ trace_id: view.trace_id,
73
+ referer: view.referer,
74
+ source: view.source,
75
+ created_at: view.created_at
76
+ }
77
+ end
83
78
  end
84
79
  end
85
80
  end
@@ -12,7 +12,7 @@ module Trackguard
12
12
  Rails.cache.delete(BlockedUserAgent::CACHE_KEY)
13
13
  render json: { status: "ok", pattern: record.pattern }
14
14
  rescue ActionController::ParameterMissing, ActiveRecord::RecordInvalid => e
15
- render json: { status: "error", message: e.message }, status: :unprocessable_entity
15
+ render json: { status: "error", message: e.message }, status: :unprocessable_content
16
16
  end
17
17
 
18
18
  private
@@ -1,16 +1,22 @@
1
1
  module Trackguard
2
2
  module Admin
3
3
  class DashboardsController < BaseController
4
- def show
5
- @total_today = PageView.today.count
6
- @total_week = PageView.this_week.count
7
- @total_month = PageView.this_month.count
4
+ include Overridable
8
5
 
9
- @top_pages = PageView.last_30.group(:path).order("count_all DESC").limit(5).count
10
- @top_referrers = PageView.last_30.with_referrer.group(:referer).order("count_all DESC").limit(5).count
11
- @top_sources = PageView.last_30.with_source.group(:source).order("count_all DESC").limit(5).count
6
+ def show
7
+ query = AnalyticsQuery.call(
8
+ scope: page_view_scope,
9
+ time_scope: page_view_scope.last_30,
10
+ limit: 5
11
+ )
12
12
 
13
- @recent = PageView.order(created_at: :desc).limit(20).includes(visitor: :whitelisted_ip)
13
+ @total_today = query.totals[:today]
14
+ @total_week = query.totals[:week]
15
+ @total_month = query.totals[:month]
16
+ @top_pages = query.top_pages
17
+ @top_referrers = query.top_referrers
18
+ @top_sources = query.top_sources
19
+ @recent = query.recent
14
20
  end
15
21
  end
16
22
  end
@@ -1,12 +1,14 @@
1
1
  module Trackguard
2
2
  module Admin
3
3
  class VisitorsController < BaseController
4
+ include Overridable
5
+
4
6
  skip_before_action :verify_authenticity_token, if: :valid_api_token?
5
7
  before_action :set_visitor
6
8
 
7
9
  rescue_from ActiveRecord::RecordNotFound do
8
10
  respond_to do |format|
9
- format.html { redirect_to dashboard_path, alert: "Visitor not found." }
11
+ format.html { redirect_to after_action_path, alert: "Visitor not found." }
10
12
  format.json { render json: { error: "Visitor not found" }, status: :not_found }
11
13
  end
12
14
  end
@@ -20,13 +22,13 @@ module Trackguard
20
22
  name: params[:name].presence || BlockedUserAgent.matching_pattern(@visitor.user_agent)
21
23
  )
22
24
  respond_to do |format|
23
- format.html { redirect_back_or_to dashboard_path }
25
+ format.html { redirect_back_or_to after_action_path }
24
26
  format.json { render json: { status: "ok", ip: @visitor.ip, flagged_at: @visitor.flagged_at } }
25
27
  end
26
28
  else
27
29
  respond_to do |format|
28
- format.html { redirect_back_or_to dashboard_path, alert: @visitor.errors.full_messages.join(", ") }
29
- format.json { render json: { errors: @visitor.errors.full_messages }, status: :unprocessable_entity }
30
+ format.html { redirect_back_or_to after_action_path, alert: @visitor.errors.full_messages.join(", ") }
31
+ format.json { render json: { errors: @visitor.errors.full_messages }, status: :unprocessable_content }
30
32
  end
31
33
  end
32
34
  end
@@ -35,7 +37,7 @@ module Trackguard
35
37
  def unflag
36
38
  @visitor.update!(flagged_at: nil, flag_reason: nil, flagged_by: nil)
37
39
  respond_to do |format|
38
- format.html { redirect_back_or_to dashboard_path }
40
+ format.html { redirect_back_or_to after_action_path }
39
41
  format.json { render json: { status: "ok", ip: @visitor.ip } }
40
42
  end
41
43
  end
@@ -47,14 +49,6 @@ module Trackguard
47
49
 
48
50
  super
49
51
  end
50
-
51
- def set_visitor
52
- @visitor = if params[:ip].present?
53
- Visitor.find_by!(ip: params[:ip])
54
- else
55
- Visitor.find(params[:id])
56
- end
57
- end
58
52
  end
59
53
  end
60
54
  end
@@ -1,16 +1,18 @@
1
1
  module Trackguard
2
2
  module Admin
3
3
  class VisitsController < BaseController
4
+ include Overridable
5
+
4
6
  PER_PAGE = 20
5
7
 
6
8
  def index
7
9
  @page = [ (params[:page] || 1).to_i, 1 ].max
8
- @total = PageView.count
10
+ @total = page_view_scope.count
9
11
  @pages = (@total.to_f / PER_PAGE).ceil
10
- @visits = PageView.order(created_at: :desc)
11
- .limit(PER_PAGE)
12
- .offset((@page - 1) * PER_PAGE)
13
- .includes(visitor: :whitelisted_ip)
12
+ @visits = page_view_scope.order(created_at: :desc)
13
+ .limit(PER_PAGE)
14
+ .offset((@page - 1) * PER_PAGE)
15
+ .includes(visitor: :whitelisted_ip)
14
16
  end
15
17
  end
16
18
  end
@@ -1,12 +1,14 @@
1
1
  module Trackguard
2
2
  module Admin
3
3
  class WhitelistedIpsController < BaseController
4
+ include Overridable
5
+
4
6
  skip_before_action :verify_authenticity_token, if: :valid_api_token?
5
7
  before_action :set_visitor
6
8
 
7
9
  rescue_from ActiveRecord::RecordNotFound do
8
10
  respond_to do |format|
9
- format.html { redirect_to dashboard_path, alert: "Visitor not found." }
11
+ format.html { redirect_to after_action_path, alert: "Visitor not found." }
10
12
  format.json { render json: { error: "Visitor not found" }, status: :not_found }
11
13
  end
12
14
  end
@@ -17,13 +19,13 @@ module Trackguard
17
19
  record.expires_at = params[:expires_at].presence || 7.days.from_now
18
20
  record.save!
19
21
  respond_to do |format|
20
- format.html { redirect_back_or_to dashboard_path }
22
+ format.html { redirect_back_or_to after_action_path }
21
23
  format.json { render json: { status: "ok", ip: @visitor.ip, expires_at: record.expires_at } }
22
24
  end
23
25
  rescue ActiveRecord::RecordInvalid => e
24
26
  respond_to do |format|
25
- format.html { redirect_back_or_to dashboard_path, alert: e.message }
26
- format.json { render json: { status: "error", message: e.message }, status: :unprocessable_entity }
27
+ format.html { redirect_back_or_to after_action_path, alert: e.message }
28
+ format.json { render json: { status: "error", message: e.message }, status: :unprocessable_content }
27
29
  end
28
30
  end
29
31
 
@@ -33,12 +35,12 @@ module Trackguard
33
35
  if record
34
36
  record.destroy!
35
37
  respond_to do |format|
36
- format.html { redirect_back_or_to dashboard_path }
38
+ format.html { redirect_back_or_to after_action_path }
37
39
  format.json { render json: { status: "ok", ip: @visitor.ip } }
38
40
  end
39
41
  else
40
42
  respond_to do |format|
41
- format.html { redirect_back_or_to dashboard_path, alert: "No whitelist entry found." }
43
+ format.html { redirect_back_or_to after_action_path, alert: "No whitelist entry found." }
42
44
  format.json { render json: { error: "Not whitelisted" }, status: :not_found }
43
45
  end
44
46
  end
@@ -51,14 +53,6 @@ module Trackguard
51
53
 
52
54
  super
53
55
  end
54
-
55
- def set_visitor
56
- @visitor = if params[:ip].present?
57
- Visitor.find_by!(ip: params[:ip])
58
- else
59
- Visitor.find(params[:id])
60
- end
61
- end
62
56
  end
63
57
  end
64
58
  end
@@ -0,0 +1,27 @@
1
+ module Trackguard
2
+ class AnalyticsQuery < ApplicationService
3
+ attr_reader :totals, :top_pages, :top_referrers, :top_sources, :recent
4
+
5
+ def initialize(scope:, time_scope:, limit:)
6
+ @scope = scope
7
+ @time_scope = time_scope
8
+ @limit = limit
9
+ end
10
+
11
+ def call
12
+ @totals = {
13
+ today: @scope.today.count,
14
+ week: @scope.this_week.count,
15
+ month: @scope.this_month.count
16
+ }
17
+
18
+ @top_pages = @time_scope.group(:path).order("count_all DESC").limit(@limit).count
19
+ @top_referrers = @time_scope.with_referrer.group(:referer).order("count_all DESC").limit(@limit).count
20
+ @top_sources = @time_scope.with_source.group(:source).order("count_all DESC").limit(@limit).count
21
+
22
+ @recent = @scope.order(created_at: :desc).limit(20).includes(visitor: :whitelisted_ip)
23
+
24
+ self
25
+ end
26
+ end
27
+ end
@@ -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>
@@ -2,6 +2,7 @@ class CreateTrackguardTables < ActiveRecord::Migration[<%= ActiveRecord::Migrati
2
2
  def change
3
3
  create_table :trackguard_visitors do |t|
4
4
  t.string :ip
5
+ t.string :name
5
6
  t.string :user_agent
6
7
  t.datetime :first_seen_at, null: false
7
8
  t.datetime :last_seen_at, null: false
@@ -1,3 +1,3 @@
1
1
  module Trackguard
2
- VERSION = "0.19.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.19.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Krzysztof Rygielski
@@ -46,6 +46,7 @@ files:
46
46
  - app/assets/images/trackguard/logo.png
47
47
  - app/assets/javascripts/controllers/page_tracker_controller.js
48
48
  - app/assets/stylesheets/trackguard/admin.css
49
+ - app/controllers/concerns/trackguard/admin/overridable.rb
49
50
  - app/controllers/concerns/trackguard/page_tracker.rb
50
51
  - app/controllers/trackguard/admin/analytics_controller.rb
51
52
  - app/controllers/trackguard/admin/base_controller.rb
@@ -65,9 +66,11 @@ files:
65
66
  - app/models/trackguard/visit.rb
66
67
  - app/models/trackguard/visitor.rb
67
68
  - app/models/trackguard/whitelisted_ip.rb
69
+ - app/services/trackguard/analytics_query.rb
68
70
  - app/services/trackguard/application_service.rb
69
71
  - app/services/trackguard/page_view_recorder.rb
70
72
  - app/views/layouts/trackguard/admin.html.erb
73
+ - app/views/trackguard/admin/_visit_row.html.erb
71
74
  - app/views/trackguard/admin/dashboards/show.html.erb
72
75
  - app/views/trackguard/admin/visits/_pagination.html.erb
73
76
  - app/views/trackguard/admin/visits/index.html.erb