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 +4 -4
- data/app/assets/stylesheets/trackguard/admin.css +76 -60
- data/app/controllers/concerns/trackguard/admin/overridable.rb +29 -0
- data/app/controllers/trackguard/admin/analytics_controller.rb +30 -35
- data/app/controllers/trackguard/admin/blocked_user_agents_controller.rb +1 -1
- data/app/controllers/trackguard/admin/dashboards_controller.rb +14 -8
- data/app/controllers/trackguard/admin/visitors_controller.rb +7 -13
- data/app/controllers/trackguard/admin/visits_controller.rb +7 -5
- data/app/controllers/trackguard/admin/whitelisted_ips_controller.rb +8 -14
- data/app/services/trackguard/analytics_query.rb +27 -0
- data/app/views/trackguard/admin/_visit_row.html.erb +133 -0
- data/app/views/trackguard/admin/dashboards/show.html.erb +1 -132
- data/app/views/trackguard/admin/visits/index.html.erb +1 -132
- data/lib/generators/trackguard/templates/create_trackguard_tables.rb +1 -0
- data/lib/trackguard/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c250ded678f9970940e87fff8a0d46ff58f8e504a0f63b1abcdd3a2a7955ec7
|
|
4
|
+
data.tar.gz: 33d162a61e298288facf7f02424864b8f0588cd7f6cfce437cb866f6c82944e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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: #
|
|
11
|
-
color: #
|
|
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: #
|
|
26
|
-
border-bottom: 1px solid
|
|
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: #
|
|
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: #
|
|
74
|
+
color: #f3f4f6;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/* ── Nav ───────────────────────────────────────────────────────────── */
|
|
78
78
|
.tg-nav {
|
|
79
|
-
background: #
|
|
80
|
-
border-bottom: 1px solid #
|
|
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: #
|
|
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: #
|
|
97
|
+
.tg-nav__link:hover { color: #111827; }
|
|
98
98
|
|
|
99
99
|
.tg-nav__link--active {
|
|
100
|
-
color: #
|
|
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: #
|
|
119
|
+
color: #111827;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/* ── Stats ─────────────────────────────────────────────────────────── */
|
|
@@ -128,10 +128,11 @@
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
.tg-stat {
|
|
131
|
-
background: #
|
|
132
|
-
border: 1px solid #
|
|
133
|
-
border-radius:
|
|
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: #
|
|
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: #
|
|
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: #
|
|
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: #
|
|
171
|
-
border: 1px solid #
|
|
172
|
-
border-radius:
|
|
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: #
|
|
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: #
|
|
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: #
|
|
214
|
+
color: #374151;
|
|
213
215
|
text-decoration: none;
|
|
214
|
-
background: #
|
|
216
|
+
background: #ffffff;
|
|
217
|
+
border: 1px solid #e5e7eb;
|
|
215
218
|
}
|
|
216
219
|
|
|
217
|
-
.tg-pagination__link:hover { background: #
|
|
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: #
|
|
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: #
|
|
252
|
-
border-bottom: 1px solid #
|
|
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 #
|
|
260
|
-
color: #
|
|
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: #
|
|
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 #
|
|
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: #
|
|
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(
|
|
291
|
+
background: rgba(239, 68, 68, 0.05);
|
|
288
292
|
}
|
|
289
293
|
|
|
290
294
|
.tg-row--whitelisted {
|
|
291
|
-
background: rgba(
|
|
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: #
|
|
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: #
|
|
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: #
|
|
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: #
|
|
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: #
|
|
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: #
|
|
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: #
|
|
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: #
|
|
412
|
+
color: #9ca3af;
|
|
398
413
|
width: 6rem;
|
|
399
414
|
flex-shrink: 0;
|
|
400
415
|
}
|
|
401
416
|
|
|
402
417
|
.tg-dl__def {
|
|
403
|
-
color: #
|
|
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: #
|
|
429
|
+
.tg-dl__def--flagged { color: #dc2626; }
|
|
415
430
|
|
|
416
|
-
.tg-dl__def--muted { color: #
|
|
431
|
+
.tg-dl__def--muted { color: #9ca3af; }
|
|
417
432
|
|
|
418
|
-
.tg-dl__def--whitelisted { color: #
|
|
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 #
|
|
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: #
|
|
440
|
-
border: 1px solid #
|
|
454
|
+
background: #ffffff;
|
|
455
|
+
border: 1px solid #d1d5db;
|
|
441
456
|
border-radius: 4px;
|
|
442
|
-
color: #
|
|
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: #
|
|
464
|
+
.tg-input::placeholder { color: #9ca3af; }
|
|
450
465
|
|
|
451
466
|
.tg-btn {
|
|
452
|
-
border:
|
|
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 {
|
|
462
|
-
.tg-btn--danger:hover { background:
|
|
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 {
|
|
465
|
-
.tg-btn--ghost:hover { background: #
|
|
480
|
+
.tg-btn--ghost { border-color: #d1d5db; color: #374151; background: #ffffff; }
|
|
481
|
+
.tg-btn--ghost:hover { background: #f3f4f6; }
|
|
466
482
|
|
|
467
|
-
.tg-btn--whitelist {
|
|
468
|
-
.tg-btn--whitelist:hover { background:
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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:
|
|
23
|
-
top_pages:
|
|
24
|
-
top_referrers:
|
|
25
|
-
top_sources:
|
|
26
|
-
recent:
|
|
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
|
|
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
|
-
|
|
68
|
-
scope = scope.joins(:visitor).merge(
|
|
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: :
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
@
|
|
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
|
|
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
|
|
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
|
|
29
|
-
format.json { render json: { errors: @visitor.errors.full_messages }, status: :
|
|
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
|
|
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 =
|
|
10
|
+
@total = page_view_scope.count
|
|
9
11
|
@pages = (@total.to_f / PER_PAGE).ceil
|
|
10
|
-
@visits =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
|
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
|
|
26
|
-
format.json { render json: { status: "error", message: e.message }, status: :
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/trackguard/version.rb
CHANGED
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.
|
|
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
|