findbug 0.2.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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +8 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +375 -0
  6. data/Rakefile +12 -0
  7. data/app/controllers/findbug/application_controller.rb +105 -0
  8. data/app/controllers/findbug/dashboard_controller.rb +93 -0
  9. data/app/controllers/findbug/errors_controller.rb +129 -0
  10. data/app/controllers/findbug/performance_controller.rb +80 -0
  11. data/app/jobs/findbug/alert_job.rb +40 -0
  12. data/app/jobs/findbug/cleanup_job.rb +132 -0
  13. data/app/jobs/findbug/persist_job.rb +158 -0
  14. data/app/models/findbug/error_event.rb +197 -0
  15. data/app/models/findbug/performance_event.rb +237 -0
  16. data/app/views/findbug/dashboard/index.html.erb +199 -0
  17. data/app/views/findbug/errors/index.html.erb +137 -0
  18. data/app/views/findbug/errors/show.html.erb +185 -0
  19. data/app/views/findbug/performance/index.html.erb +168 -0
  20. data/app/views/findbug/performance/show.html.erb +203 -0
  21. data/app/views/layouts/findbug/application.html.erb +601 -0
  22. data/lib/findbug/alerts/channels/base.rb +75 -0
  23. data/lib/findbug/alerts/channels/discord.rb +155 -0
  24. data/lib/findbug/alerts/channels/email.rb +179 -0
  25. data/lib/findbug/alerts/channels/slack.rb +149 -0
  26. data/lib/findbug/alerts/channels/webhook.rb +143 -0
  27. data/lib/findbug/alerts/dispatcher.rb +126 -0
  28. data/lib/findbug/alerts/throttler.rb +110 -0
  29. data/lib/findbug/background_persister.rb +142 -0
  30. data/lib/findbug/capture/context.rb +301 -0
  31. data/lib/findbug/capture/exception_handler.rb +141 -0
  32. data/lib/findbug/capture/exception_subscriber.rb +228 -0
  33. data/lib/findbug/capture/message_handler.rb +104 -0
  34. data/lib/findbug/capture/middleware.rb +247 -0
  35. data/lib/findbug/configuration.rb +381 -0
  36. data/lib/findbug/engine.rb +109 -0
  37. data/lib/findbug/performance/instrumentation.rb +336 -0
  38. data/lib/findbug/performance/transaction.rb +193 -0
  39. data/lib/findbug/processing/data_scrubber.rb +163 -0
  40. data/lib/findbug/rails/controller_methods.rb +152 -0
  41. data/lib/findbug/railtie.rb +222 -0
  42. data/lib/findbug/storage/circuit_breaker.rb +223 -0
  43. data/lib/findbug/storage/connection_pool.rb +134 -0
  44. data/lib/findbug/storage/redis_buffer.rb +285 -0
  45. data/lib/findbug/tasks/findbug.rake +167 -0
  46. data/lib/findbug/version.rb +5 -0
  47. data/lib/findbug.rb +216 -0
  48. data/lib/generators/findbug/install_generator.rb +67 -0
  49. data/lib/generators/findbug/templates/POST_INSTALL +41 -0
  50. data/lib/generators/findbug/templates/create_findbug_error_events.rb +44 -0
  51. data/lib/generators/findbug/templates/create_findbug_performance_events.rb +47 -0
  52. data/lib/generators/findbug/templates/initializer.rb +157 -0
  53. data/sig/findbug.rbs +4 -0
  54. metadata +251 -0
@@ -0,0 +1,601 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Findbug - Error & Performance Monitoring</title>
7
+
8
+ <%# shadcn/ui Neutral Dark Theme %>
9
+ <style>
10
+ /* ========================================
11
+ CSS Variables (shadcn neutral dark)
12
+ ======================================== */
13
+ :root {
14
+ --background: 0 0% 3.9%;
15
+ --foreground: 0 0% 98%;
16
+ --card: 0 0% 3.9%;
17
+ --card-foreground: 0 0% 98%;
18
+ --popover: 0 0% 3.9%;
19
+ --popover-foreground: 0 0% 98%;
20
+ --primary: 0 0% 98%;
21
+ --primary-foreground: 0 0% 9%;
22
+ --secondary: 0 0% 14.9%;
23
+ --secondary-foreground: 0 0% 98%;
24
+ --muted: 0 0% 14.9%;
25
+ --muted-foreground: 0 0% 63.9%;
26
+ --accent: 0 0% 14.9%;
27
+ --accent-foreground: 0 0% 98%;
28
+ --destructive: 0 62.8% 30.6%;
29
+ --destructive-foreground: 0 0% 98%;
30
+ --border: 0 0% 14.9%;
31
+ --input: 0 0% 14.9%;
32
+ --ring: 0 0% 83.1%;
33
+ --success: 142.1 76.2% 36.3%;
34
+ --warning: 38 92% 50%;
35
+ --error: 0 84.2% 60.2%;
36
+ --info: 217.2 91.2% 59.8%;
37
+ --radius: 0.5rem;
38
+ }
39
+
40
+ * { box-sizing: border-box; margin: 0; padding: 0; }
41
+
42
+ body {
43
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
44
+ background-color: hsl(var(--background));
45
+ color: hsl(var(--foreground));
46
+ line-height: 1.5;
47
+ min-height: 100vh;
48
+ -webkit-font-smoothing: antialiased;
49
+ -moz-osx-font-smoothing: grayscale;
50
+ }
51
+
52
+ a { color: inherit; text-decoration: none; transition: color 0.15s; }
53
+
54
+ .container { max-width: 1400px; margin: 0 auto; padding: 0 1.5rem; }
55
+
56
+ /* Header - shadcn style */
57
+ header {
58
+ background-color: hsl(var(--background));
59
+ position: sticky;
60
+ top: 0;
61
+ z-index: 50;
62
+ }
63
+
64
+ .header-content {
65
+ display: flex;
66
+ align-items: center;
67
+ height: 3.5rem;
68
+ gap: 1.5rem;
69
+ }
70
+
71
+ .logo {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 0.5rem;
75
+ font-size: 0.875rem;
76
+ font-weight: 600;
77
+ color: hsl(var(--foreground));
78
+ padding-right: 1rem;
79
+ border-right: 1px solid hsl(var(--border));
80
+ }
81
+ .logo svg { width: 1.25rem; height: 1.25rem; }
82
+ .logo:hover { color: hsl(var(--foreground)); }
83
+
84
+ nav {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 0.125rem;
88
+ }
89
+
90
+ nav a {
91
+ padding: 0.375rem 0.75rem;
92
+ font-size: 0.875rem;
93
+ font-weight: 500;
94
+ color: hsl(var(--muted-foreground));
95
+ border-radius: var(--radius);
96
+ transition: all 0.15s;
97
+ }
98
+
99
+ nav a:hover {
100
+ color: hsl(var(--foreground));
101
+ }
102
+
103
+ nav a.active {
104
+ color: hsl(var(--foreground));
105
+ background-color: hsl(var(--accent));
106
+ }
107
+
108
+ .header-right {
109
+ margin-left: auto;
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 0.5rem;
113
+ }
114
+
115
+ .health-indicator {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 0.375rem;
119
+ font-size: 0.75rem;
120
+ color: hsl(var(--muted-foreground));
121
+ padding: 0.25rem 0.625rem;
122
+ background-color: hsl(var(--secondary));
123
+ border-radius: var(--radius);
124
+ }
125
+
126
+ .health-dot {
127
+ width: 0.5rem;
128
+ height: 0.5rem;
129
+ border-radius: 50%;
130
+ background-color: hsl(var(--success));
131
+ }
132
+
133
+ /* Main */
134
+ main { padding: 2.5rem 0 3rem; }
135
+
136
+ h1 {
137
+ font-size: 1.875rem;
138
+ font-weight: 700;
139
+ letter-spacing: -0.025em;
140
+ margin-bottom: 0.5rem;
141
+ }
142
+
143
+ .page-description {
144
+ color: hsl(var(--muted-foreground));
145
+ font-size: 0.875rem;
146
+ margin-bottom: 1.5rem;
147
+ }
148
+
149
+ h2 { font-size: 0.875rem; font-weight: 600; }
150
+
151
+ /* Cards */
152
+ .card {
153
+ background-color: hsl(var(--card));
154
+ border: 1px solid hsl(var(--border));
155
+ border-radius: var(--radius);
156
+ margin-bottom: 1.5rem;
157
+ }
158
+
159
+ .card-header {
160
+ display: flex;
161
+ justify-content: space-between;
162
+ align-items: center;
163
+ padding: 1rem 1.25rem;
164
+ border-bottom: 1px solid hsl(var(--border));
165
+ }
166
+
167
+ .card-title { font-size: 0.875rem; font-weight: 600; }
168
+ .card-description { font-size: 0.75rem; color: hsl(var(--muted-foreground)); margin-top: 0.125rem; }
169
+
170
+ .card-content { padding: 1.25rem; }
171
+
172
+ /* Stats */
173
+ .stats-grid {
174
+ display: grid;
175
+ grid-template-columns: repeat(4, 1fr);
176
+ gap: 1rem;
177
+ margin-bottom: 1.5rem;
178
+ }
179
+
180
+ @media (max-width: 1024px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } }
181
+ @media (max-width: 640px) { .stats-grid { grid-template-columns: 1fr; } }
182
+
183
+ .stat-card {
184
+ background-color: hsl(var(--card));
185
+ border: 1px solid hsl(var(--border));
186
+ border-radius: var(--radius);
187
+ padding: 1.25rem;
188
+ }
189
+
190
+ .stat-label {
191
+ font-size: 0.75rem;
192
+ font-weight: 500;
193
+ color: hsl(var(--muted-foreground));
194
+ }
195
+
196
+ .stat-value {
197
+ font-size: 2rem;
198
+ font-weight: 700;
199
+ letter-spacing: -0.025em;
200
+ margin-top: 0.25rem;
201
+ line-height: 1;
202
+ }
203
+
204
+ .stat-change {
205
+ font-size: 0.75rem;
206
+ margin-top: 0.5rem;
207
+ color: hsl(var(--muted-foreground));
208
+ }
209
+
210
+ .stat-value.error { color: hsl(var(--error)); }
211
+ .stat-value.warning { color: hsl(var(--warning)); }
212
+ .stat-value.success { color: hsl(var(--success)); }
213
+
214
+ /* Badges */
215
+ .badge {
216
+ display: inline-flex;
217
+ align-items: center;
218
+ padding: 0.125rem 0.5rem;
219
+ font-size: 0.6875rem;
220
+ font-weight: 500;
221
+ border-radius: 9999px;
222
+ }
223
+
224
+ .badge-error { background-color: hsl(var(--error) / 0.15); color: hsl(var(--error)); }
225
+ .badge-warning { background-color: hsl(var(--warning) / 0.15); color: hsl(var(--warning)); }
226
+ .badge-success { background-color: hsl(var(--success) / 0.15); color: hsl(var(--success)); }
227
+ .badge-info { background-color: hsl(var(--info) / 0.15); color: hsl(var(--info)); }
228
+ .badge-muted { background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground)); }
229
+
230
+ /* Buttons */
231
+ .btn {
232
+ display: inline-flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ gap: 0.375rem;
236
+ padding: 0.5rem 1rem;
237
+ font-size: 0.875rem;
238
+ font-weight: 500;
239
+ border-radius: var(--radius);
240
+ cursor: pointer;
241
+ transition: all 0.15s;
242
+ border: none;
243
+ text-decoration: none;
244
+ }
245
+
246
+ .btn:hover { opacity: 0.9; }
247
+
248
+ .btn-primary { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); }
249
+ .btn-secondary { background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); border: 1px solid hsl(var(--border)); }
250
+ .btn-destructive { background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); }
251
+ .btn-ghost { background: transparent; color: hsl(var(--foreground)); }
252
+ .btn-ghost:hover { background-color: hsl(var(--accent)); }
253
+ .btn-outline { background: transparent; color: hsl(var(--foreground)); border: 1px solid hsl(var(--border)); }
254
+ .btn-outline:hover { background-color: hsl(var(--accent)); }
255
+
256
+ .btn-sm { padding: 0.25rem 0.625rem; font-size: 0.75rem; height: 1.75rem; }
257
+
258
+ /* Tables */
259
+ .table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
260
+
261
+ .table th {
262
+ padding: 0.75rem 1rem;
263
+ text-align: left;
264
+ font-weight: 500;
265
+ font-size: 0.75rem;
266
+ color: hsl(var(--muted-foreground));
267
+ border-bottom: 1px solid hsl(var(--border));
268
+ }
269
+
270
+ .table td {
271
+ padding: 0.75rem 1rem;
272
+ border-bottom: 1px solid hsl(var(--border));
273
+ vertical-align: middle;
274
+ }
275
+
276
+ .table tbody tr { transition: background-color 0.15s; }
277
+ .table tbody tr:hover { background-color: hsl(var(--muted) / 0.5); }
278
+ .table tbody tr:last-child td { border-bottom: none; }
279
+
280
+ .table a { color: hsl(var(--foreground)); }
281
+ .table a:hover { text-decoration: underline; }
282
+
283
+ /* Inputs */
284
+ select, input[type="text"], input[type="search"] {
285
+ height: 2rem;
286
+ padding: 0 0.75rem;
287
+ font-size: 0.875rem;
288
+ background-color: hsl(var(--background));
289
+ border: 1px solid hsl(var(--input));
290
+ border-radius: var(--radius);
291
+ color: hsl(var(--foreground));
292
+ transition: border-color 0.15s;
293
+ }
294
+
295
+ select:focus, input:focus { outline: none; border-color: hsl(var(--ring)); box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2); }
296
+
297
+ select {
298
+ cursor: pointer;
299
+ appearance: none;
300
+ padding-right: 2rem;
301
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23a1a1aa' d='M2.5 4.5L6 8l3.5-3.5'/%3E%3C/svg%3E");
302
+ background-repeat: no-repeat;
303
+ background-position: right 0.5rem center;
304
+ }
305
+
306
+ .filters { display: flex; gap: 0.75rem; margin-bottom: 1.25rem; flex-wrap: wrap; align-items: center; }
307
+ .filter-group { display: flex; align-items: center; gap: 0.5rem; }
308
+ .filter-group label { font-size: 0.875rem; color: hsl(var(--muted-foreground)); }
309
+
310
+ /* Filter bar - single row layout */
311
+ .filter-bar {
312
+ display: flex;
313
+ align-items: center;
314
+ justify-content: space-between;
315
+ gap: 1rem;
316
+ margin-bottom: 1.5rem;
317
+ flex-wrap: wrap;
318
+ }
319
+
320
+ .filter-bar-filters {
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 0.5rem;
324
+ flex-wrap: wrap;
325
+ }
326
+
327
+ .filter-bar-search {
328
+ display: flex;
329
+ align-items: center;
330
+ gap: 0.5rem;
331
+ }
332
+
333
+ .filter-bar-search input[type="text"] {
334
+ width: 200px;
335
+ }
336
+
337
+ @media (max-width: 768px) {
338
+ .filter-bar { flex-direction: column; align-items: stretch; }
339
+ .filter-bar-filters { flex-wrap: wrap; }
340
+ .filter-bar-search { width: 100%; }
341
+ .filter-bar-search input[type="text"] { flex: 1; width: auto; }
342
+ }
343
+
344
+ /* Code block */
345
+ .code-block {
346
+ background-color: hsl(0 0% 7%);
347
+ border: 1px solid hsl(var(--border));
348
+ border-radius: var(--radius);
349
+ padding: 1rem;
350
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
351
+ font-size: 0.8125rem;
352
+ line-height: 1.6;
353
+ overflow-x: auto;
354
+ max-width: 100%;
355
+ }
356
+
357
+ .code-block pre {
358
+ margin: 0;
359
+ white-space: pre-wrap;
360
+ word-break: break-all;
361
+ }
362
+
363
+ .code-line { display: block; padding: 0.0625rem 0; }
364
+ .code-line:hover { background-color: hsl(var(--muted) / 0.3); margin: 0 -1rem; padding: 0.0625rem 1rem; }
365
+ .code-line-number { display: inline-block; width: 2.5rem; color: hsl(var(--muted-foreground)); user-select: none; text-align: right; margin-right: 1rem; opacity: 0.5; }
366
+
367
+ /* Flash */
368
+ .flash {
369
+ display: flex;
370
+ align-items: center;
371
+ gap: 0.5rem;
372
+ padding: 0.75rem 1rem;
373
+ border-radius: var(--radius);
374
+ margin-bottom: 1rem;
375
+ font-size: 0.875rem;
376
+ }
377
+
378
+ .flash-success { background-color: hsl(var(--success) / 0.15); color: hsl(var(--success)); border: 1px solid hsl(var(--success) / 0.2); }
379
+ .flash-error { background-color: hsl(var(--error) / 0.15); color: hsl(var(--error)); border: 1px solid hsl(var(--error) / 0.2); }
380
+
381
+ /* Pagination */
382
+ .pagination { display: flex; gap: 0.25rem; margin-top: 1.5rem; justify-content: center; }
383
+
384
+ .pagination a, .pagination span {
385
+ display: inline-flex;
386
+ align-items: center;
387
+ justify-content: center;
388
+ min-width: 2rem;
389
+ height: 2rem;
390
+ padding: 0 0.5rem;
391
+ font-size: 0.875rem;
392
+ border-radius: var(--radius);
393
+ border: 1px solid hsl(var(--border));
394
+ color: hsl(var(--muted-foreground));
395
+ transition: all 0.15s;
396
+ }
397
+
398
+ .pagination a:hover { background-color: hsl(var(--accent)); color: hsl(var(--foreground)); }
399
+ .pagination .current { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-color: transparent; }
400
+ .pagination .disabled { opacity: 0.5; pointer-events: none; }
401
+
402
+ /* Grid */
403
+ .grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.5rem; }
404
+ .grid-3-1 { display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem; }
405
+ .grid-3-1 > * { min-width: 0; overflow: hidden; }
406
+ @media (max-width: 1024px) { .grid-2, .grid-3-1 { grid-template-columns: 1fr; } }
407
+
408
+ /* Text helpers */
409
+ .text-muted { color: hsl(var(--muted-foreground)); }
410
+ .text-xs { font-size: 0.75rem; }
411
+ .text-sm { font-size: 0.875rem; }
412
+ .font-mono { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; }
413
+
414
+ /* Status dot */
415
+ .status-dot { display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 50%; margin-right: 0.375rem; }
416
+ .status-dot.success { background-color: hsl(var(--success)); }
417
+ .status-dot.error { background-color: hsl(var(--error)); }
418
+ .status-dot.warning { background-color: hsl(var(--warning)); }
419
+
420
+ /* Empty state */
421
+ .empty-state { text-align: center; padding: 3rem 1.5rem; color: hsl(var(--muted-foreground)); }
422
+ .empty-state-icon { font-size: 2rem; margin-bottom: 0.75rem; opacity: 0.5; }
423
+ .empty-state p { font-size: 0.875rem; }
424
+
425
+ /* Chart - Area style */
426
+ .chart-container {
427
+ height: 140px;
428
+ display: flex;
429
+ align-items: flex-end;
430
+ gap: 1px;
431
+ padding: 1rem 0 0.5rem;
432
+ background: transparent;
433
+ position: relative;
434
+ }
435
+ .chart-container::before {
436
+ content: '';
437
+ position: absolute;
438
+ top: 1rem;
439
+ left: 0;
440
+ right: 0;
441
+ bottom: 0.5rem;
442
+ background: linear-gradient(to bottom, hsl(var(--muted) / 0.3) 0%, transparent 100%);
443
+ border-top: 1px dashed hsl(var(--border));
444
+ pointer-events: none;
445
+ }
446
+ .chart-bar {
447
+ flex: 1;
448
+ background: linear-gradient(to top, hsl(217 91% 60% / 0.1) 0%, hsl(217 91% 60% / 0.4) 100%);
449
+ border-top: 2px solid hsl(217 91% 60%);
450
+ min-width: 2px;
451
+ transition: all 0.15s;
452
+ position: relative;
453
+ z-index: 1;
454
+ }
455
+ .chart-bar:hover {
456
+ background: linear-gradient(to top, hsl(217 91% 60% / 0.2) 0%, hsl(217 91% 60% / 0.6) 100%);
457
+ border-top-color: hsl(217 91% 70%);
458
+ }
459
+ .chart-bar::after {
460
+ content: attr(data-tooltip);
461
+ position: absolute;
462
+ bottom: 100%;
463
+ left: 50%;
464
+ transform: translateX(-50%);
465
+ background: hsl(var(--popover));
466
+ border: 1px solid hsl(var(--border));
467
+ padding: 0.375rem 0.5rem;
468
+ border-radius: var(--radius);
469
+ font-size: 0.75rem;
470
+ white-space: nowrap;
471
+ opacity: 0;
472
+ pointer-events: none;
473
+ transition: opacity 0.15s;
474
+ z-index: 10;
475
+ }
476
+ .chart-bar:hover::after { opacity: 1; }
477
+ .chart-labels {
478
+ display: flex;
479
+ justify-content: space-between;
480
+ padding-top: 0.5rem;
481
+ border-top: 1px solid hsl(var(--border));
482
+ }
483
+
484
+ /* Progress bar */
485
+ .progress { height: 0.375rem; background-color: hsl(var(--muted)); border-radius: 9999px; overflow: hidden; }
486
+ .progress-bar { height: 100%; border-radius: 9999px; }
487
+ .progress-bar.info { background-color: hsl(var(--info)); }
488
+ .progress-bar.warning { background-color: hsl(var(--warning)); }
489
+ .progress-bar.error { background-color: hsl(var(--error)); }
490
+
491
+ /* Separator */
492
+ .separator { height: 1px; background-color: hsl(var(--border)); margin: 1rem 0; }
493
+
494
+ /* Error detail header */
495
+ .error-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 1.5rem; margin-bottom: 1.5rem; }
496
+ .error-title { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; }
497
+ .error-message { color: hsl(var(--muted-foreground)); font-size: 0.9375rem; word-break: break-word; }
498
+ .error-meta { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.75rem; }
499
+
500
+ @media (max-width: 768px) { .error-header { flex-direction: column; } }
501
+
502
+ /* Tabs */
503
+ .tabs { display: flex; gap: 0.25rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 1.5rem; }
504
+ .tab {
505
+ padding: 0.625rem 1rem;
506
+ font-size: 0.875rem;
507
+ font-weight: 500;
508
+ color: hsl(var(--muted-foreground));
509
+ border-bottom: 2px solid transparent;
510
+ margin-bottom: -1px;
511
+ transition: all 0.15s;
512
+ }
513
+ .tab:hover { color: hsl(var(--foreground)); }
514
+ .tab.active { color: hsl(var(--foreground)); border-bottom-color: hsl(var(--primary)); }
515
+
516
+ /* Skeleton loading */
517
+ .skeleton { background: linear-gradient(90deg, hsl(var(--muted)) 25%, hsl(var(--muted) / 0.5) 50%, hsl(var(--muted)) 75%); background-size: 200% 100%; animation: skeleton 1.5s ease-in-out infinite; border-radius: var(--radius); }
518
+ @keyframes skeleton { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
519
+ </style>
520
+
521
+ <%= turbo_include_tags if defined?(Turbo) && respond_to?(:turbo_include_tags) %>
522
+ <%= stimulus_include_tags if defined?(Stimulus) && respond_to?(:stimulus_include_tags) %>
523
+
524
+ <script>
525
+ // Convert UTC times to user's local timezone
526
+ document.addEventListener('DOMContentLoaded', function() {
527
+ document.querySelectorAll('.local-time').forEach(function(el) {
528
+ var utc = el.getAttribute('data-utc');
529
+ if (utc) {
530
+ var date = new Date(utc);
531
+ var options = {
532
+ year: 'numeric',
533
+ month: '2-digit',
534
+ day: '2-digit',
535
+ hour: '2-digit',
536
+ minute: '2-digit',
537
+ second: '2-digit',
538
+ timeZoneName: 'short'
539
+ };
540
+ el.textContent = date.toLocaleString(undefined, options);
541
+ }
542
+ });
543
+ });
544
+ </script>
545
+ </head>
546
+ <body>
547
+ <header>
548
+ <div class="container header-content">
549
+ <a href="<%= findbug.root_path %>" class="logo">
550
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
551
+ <path d="M8 2l1.88 1.88"/>
552
+ <path d="M14.12 3.88L16 2"/>
553
+ <path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/>
554
+ <path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/>
555
+ <path d="M12 20v-9"/>
556
+ <path d="M6.53 9C4.6 8.8 3 7.1 3 5"/>
557
+ <path d="M6 13H2"/>
558
+ <path d="M3 21c0-2.1 1.7-3.9 3.8-4"/>
559
+ <path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/>
560
+ <path d="M22 13h-4"/>
561
+ <path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/>
562
+ </svg>
563
+ <span>FindBug</span>
564
+ </a>
565
+ <nav>
566
+ <a href="<%= findbug.root_path %>" class="<%= 'active' if controller_name == 'dashboard' %>">Dashboard</a>
567
+ <a href="<%= findbug.errors_path %>" class="<%= 'active' if controller_name == 'errors' %>">Errors</a>
568
+ <a href="<%= findbug.performance_index_path %>" class="<%= 'active' if controller_name == 'performance' %>">Performance</a>
569
+ </nav>
570
+ <div class="header-right">
571
+ <div class="health-indicator">
572
+ <span class="health-dot"></span>
573
+ <span>Healthy</span>
574
+ </div>
575
+ </div>
576
+ </div>
577
+ </header>
578
+
579
+ <main class="container">
580
+ <%# Flash messages - wrapped in rescue for API-mode Rails apps without sessions %>
581
+ <% begin %>
582
+ <% if flash[:success] %>
583
+ <div class="flash flash-success">
584
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm3.78-9.72a.75.75 0 0 0-1.06-1.06L7 8.94 5.28 7.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.06 0l4.25-4.25z"/></svg>
585
+ <%= flash[:success] %>
586
+ </div>
587
+ <% end %>
588
+ <% if flash[:error] %>
589
+ <div class="flash flash-error">
590
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zM5.22 5.22a.75.75 0 0 1 1.06 0L8 6.94l1.72-1.72a.75.75 0 1 1 1.06 1.06L9.06 8l1.72 1.72a.75.75 0 1 1-1.06 1.06L8 9.06l-1.72 1.72a.75.75 0 0 1-1.06-1.06L6.94 8 5.22 6.28a.75.75 0 0 1 0-1.06z"/></svg>
591
+ <%= flash[:error] %>
592
+ </div>
593
+ <% end %>
594
+ <% rescue NoMethodError %>
595
+ <%# Flash not available (API mode), skip %>
596
+ <% end %>
597
+
598
+ <%= yield %>
599
+ </main>
600
+ </body>
601
+ </html>
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Findbug
4
+ module Alerts
5
+ module Channels
6
+ # Base is the abstract base class for alert channels.
7
+ #
8
+ # CHANNEL PATTERN
9
+ # ===============
10
+ #
11
+ # Each channel implements:
12
+ # 1. #initialize(config) - Receive channel configuration
13
+ # 2. #send_alert(error_event) - Send the alert
14
+ #
15
+ # This pattern allows adding new channels easily:
16
+ #
17
+ # class PagerDuty < Base
18
+ # def send_alert(error_event)
19
+ # # Call PagerDuty API
20
+ # end
21
+ # end
22
+ #
23
+ class Base
24
+ attr_reader :config
25
+
26
+ def initialize(config)
27
+ @config = config
28
+ end
29
+
30
+ # Send an alert for an error event
31
+ #
32
+ # @param error_event [ErrorEvent] the error to alert about
33
+ #
34
+ def send_alert(error_event)
35
+ raise NotImplementedError, "#{self.class} must implement #send_alert"
36
+ end
37
+
38
+ protected
39
+
40
+ # Format error for display
41
+ def format_error_title(error_event)
42
+ "[#{error_event.severity.upcase}] #{error_event.exception_class}"
43
+ end
44
+
45
+ def format_error_message(error_event)
46
+ error_event.message.to_s.truncate(500)
47
+ end
48
+
49
+ def format_occurrence_info(error_event)
50
+ if error_event.occurrence_count > 1
51
+ "Occurred #{error_event.occurrence_count} times"
52
+ else
53
+ "First occurrence"
54
+ end
55
+ end
56
+
57
+ def format_environment(error_event)
58
+ error_event.environment || "unknown"
59
+ end
60
+
61
+ def format_first_backtrace_line(error_event)
62
+ error_event.backtrace_lines.first || "No backtrace available"
63
+ end
64
+
65
+ # Build a URL to the error in the dashboard
66
+ def error_url(error_event)
67
+ base_url = ENV.fetch("FINDBUG_BASE_URL", nil)
68
+ return nil unless base_url
69
+
70
+ "#{base_url}#{Findbug.config.web_path}/errors/#{error_event.id}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end