devformance 0.1.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 +7 -0
- data/README.md +205 -0
- data/app/assets/builds/tailwind.css +2 -0
- data/app/assets/images/icon.png +0 -0
- data/app/assets/images/icon.svg +68 -0
- data/app/assets/stylesheets/devmetrics/dashboard.css +476 -0
- data/app/assets/stylesheets/devmetrics_live/application.css +10 -0
- data/app/assets/tailwind/application.css +1 -0
- data/app/channels/application_cable/channel.rb +4 -0
- data/app/channels/application_cable/connection.rb +4 -0
- data/app/channels/devformance/metrics_channel.rb +25 -0
- data/app/controllers/application_controller.rb +4 -0
- data/app/controllers/devformance/application_controller.rb +19 -0
- data/app/controllers/devformance/icons_controller.rb +21 -0
- data/app/controllers/devformance/metrics_controller.rb +41 -0
- data/app/controllers/devformance/playground_controller.rb +89 -0
- data/app/helpers/application_helper.rb +9 -0
- data/app/helpers/metrics_helper.rb +2 -0
- data/app/helpers/playground_helper.rb +2 -0
- data/app/javascript/devformance/channels/consumer.js +2 -0
- data/app/javascript/devformance/channels/index.js +1 -0
- data/app/javascript/devformance/controllers/application.js +9 -0
- data/app/javascript/devformance/controllers/hello_controller.js +7 -0
- data/app/javascript/devformance/controllers/index.js +14 -0
- data/app/javascript/devformance/controllers/metrics_controller.js +364 -0
- data/app/javascript/devformance/controllers/playground_controller.js +33 -0
- data/app/javascript/devmetrics.js +4 -0
- data/app/jobs/application_job.rb +7 -0
- data/app/jobs/devformance/file_runner_job.rb +318 -0
- data/app/mailers/application_mailer.rb +4 -0
- data/app/models/application_record.rb +3 -0
- data/app/models/devformance/file_result.rb +14 -0
- data/app/models/devformance/run.rb +19 -0
- data/app/models/devformance/slow_query.rb +5 -0
- data/app/views/devformance/metrics/index.html.erb +79 -0
- data/app/views/devformance/playground/run.html.erb +63 -0
- data/app/views/layouts/devformance/application.html.erb +856 -0
- data/app/views/layouts/mailer.html.erb +13 -0
- data/app/views/layouts/mailer.text.erb +1 -0
- data/app/views/metrics/index.html.erb +334 -0
- data/app/views/pwa/manifest.json.erb +22 -0
- data/app/views/pwa/service-worker.js +26 -0
- data/config/BUSINESS_LOGIC_PLAN.md +1244 -0
- data/config/application.rb +31 -0
- data/config/boot.rb +4 -0
- data/config/cable.yml +17 -0
- data/config/cache.yml +16 -0
- data/config/credentials.yml.enc +1 -0
- data/config/database.yml +98 -0
- data/config/deploy.yml +116 -0
- data/config/engine_routes.rb +13 -0
- data/config/environment.rb +5 -0
- data/config/environments/development.rb +84 -0
- data/config/environments/production.rb +90 -0
- data/config/environments/test.rb +59 -0
- data/config/importmap.rb +11 -0
- data/config/initializers/assets.rb +7 -0
- data/config/initializers/content_security_policy.rb +25 -0
- data/config/initializers/filter_parameter_logging.rb +8 -0
- data/config/initializers/inflections.rb +16 -0
- data/config/locales/en.yml +31 -0
- data/config/master.key +1 -0
- data/config/puma.rb +41 -0
- data/config/queue.yml +22 -0
- data/config/recurring.yml +15 -0
- data/config/routes.rb +20 -0
- data/config/storage.yml +34 -0
- data/db/migrate/20260317144616_create_slow_queries.rb +13 -0
- data/db/migrate/20260317175630_create_performance_runs.rb +14 -0
- data/db/migrate/20260317195043_add_run_id_to_slow_queries.rb +10 -0
- data/db/migrate/20260319000001_create_devformance_runs.rb +20 -0
- data/db/migrate/20260319000002_create_devformance_file_results.rb +29 -0
- data/db/migrate/20260319000003_add_columns_to_slow_queries.rb +7 -0
- data/lib/devformance/bullet_log_parser.rb +47 -0
- data/lib/devformance/compatibility.rb +12 -0
- data/lib/devformance/coverage_setup.rb +33 -0
- data/lib/devformance/engine.rb +80 -0
- data/lib/devformance/log_writer.rb +29 -0
- data/lib/devformance/run_orchestrator.rb +58 -0
- data/lib/devformance/sql_instrumentor.rb +29 -0
- data/lib/devformance/test_framework/base.rb +43 -0
- data/lib/devformance/test_framework/coverage_helper.rb +76 -0
- data/lib/devformance/test_framework/detector.rb +26 -0
- data/lib/devformance/test_framework/minitest.rb +71 -0
- data/lib/devformance/test_framework/registry.rb +24 -0
- data/lib/devformance/test_framework/rspec.rb +60 -0
- data/lib/devformance/test_helper.rb +42 -0
- data/lib/devformance/version.rb +3 -0
- data/lib/devformance.rb +196 -0
- data/lib/generators/devformance/install/install_generator.rb +73 -0
- data/lib/generators/devformance/install/templates/add_columns_to_slow_queries.rb.erb +7 -0
- data/lib/generators/devformance/install/templates/add_run_id_to_slow_queries.rb.erb +10 -0
- data/lib/generators/devformance/install/templates/create_devformance_file_results.rb.erb +29 -0
- data/lib/generators/devformance/install/templates/create_devformance_runs.rb.erb +20 -0
- data/lib/generators/devformance/install/templates/create_performance_runs.rb.erb +14 -0
- data/lib/generators/devformance/install/templates/create_slow_queries.rb.erb +13 -0
- data/lib/generators/devformance/install/templates/initializer.rb +23 -0
- data/lib/tasks/devformance.rake +45 -0
- data/spec/fixtures/devformance/devformance_run.rb +27 -0
- data/spec/fixtures/devformance/file_result.rb +34 -0
- data/spec/fixtures/devformance/slow_query.rb +11 -0
- data/spec/lib/devmetrics/log_writer_spec.rb +81 -0
- data/spec/lib/devmetrics/run_orchestrator_spec.rb +102 -0
- data/spec/lib/devmetrics/sql_instrumentor_spec.rb +115 -0
- data/spec/models/devmetrics/file_result_spec.rb +87 -0
- data/spec/models/devmetrics/run_spec.rb +66 -0
- data/spec/models/query_log_spec.rb +21 -0
- data/spec/rails_helper.rb +20 -0
- data/spec/requests/devmetrics/metrics_controller_spec.rb +149 -0
- data/spec/requests/devmetrics_pages_spec.rb +12 -0
- data/spec/requests/performance_spec.rb +17 -0
- data/spec/requests/slow_perf_spec.rb +9 -0
- data/spec/spec_helper.rb +114 -0
- data/spec/support/devmetrics_formatter.rb +106 -0
- data/spec/support/devmetrics_metrics.rb +37 -0
- data/spec/support/factory_bot.rb +3 -0
- metadata +200 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
DevMetrics — File Panel UI
|
|
3
|
+
============================================================ */
|
|
4
|
+
|
|
5
|
+
/* ── File row wrapper ────────────────────────────────────── */
|
|
6
|
+
.dm-file-row {
|
|
7
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.dm-file-row:last-child {
|
|
11
|
+
border-bottom: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* ── Collapsed header ────────────────────────────────────── */
|
|
15
|
+
.dm-file-header {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: 8px;
|
|
19
|
+
padding: 10px 16px;
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
user-select: none;
|
|
22
|
+
transition: background 0.1s;
|
|
23
|
+
min-height: 42px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.dm-file-header:hover {
|
|
27
|
+
background: rgba(255, 255, 255, 0.025);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* ── Chevron ─────────────────────────────────────────────── */
|
|
31
|
+
.dm-chevron {
|
|
32
|
+
font-size: 9px;
|
|
33
|
+
color: #4b5563;
|
|
34
|
+
width: 12px;
|
|
35
|
+
flex-shrink: 0;
|
|
36
|
+
transition: transform 0.15s ease;
|
|
37
|
+
line-height: 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.dm-chevron--open {
|
|
41
|
+
transform: rotate(90deg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ── Status dot ──────────────────────────────────────────── */
|
|
45
|
+
.dm-dot {
|
|
46
|
+
width: 8px;
|
|
47
|
+
height: 8px;
|
|
48
|
+
border-radius: 50%;
|
|
49
|
+
flex-shrink: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.dm-dot--pending {
|
|
53
|
+
background: #2d3748;
|
|
54
|
+
border: 1px solid #4a5568;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.dm-dot--running {
|
|
58
|
+
background: #f59e0b;
|
|
59
|
+
animation: dm-pulse 1.2s ease infinite;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.dm-dot--passed {
|
|
63
|
+
background: #10b981;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.dm-dot--failed {
|
|
67
|
+
background: #ef4444;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.dm-dot--error {
|
|
71
|
+
background: #ef4444;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@keyframes dm-pulse {
|
|
75
|
+
|
|
76
|
+
0%,
|
|
77
|
+
100% {
|
|
78
|
+
opacity: 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
50% {
|
|
82
|
+
opacity: 0.3;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ── File name ───────────────────────────────────────────── */
|
|
87
|
+
.dm-file-name {
|
|
88
|
+
flex: 1;
|
|
89
|
+
font-family: "JetBrains Mono", "SF Mono", "Fira Code", monospace;
|
|
90
|
+
font-size: 12.5px;
|
|
91
|
+
color: #cbd5e1;
|
|
92
|
+
white-space: nowrap;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
text-overflow: ellipsis;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ── Header right meta (duration + badges) ───────────────── */
|
|
98
|
+
.dm-file-meta {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: 4px;
|
|
102
|
+
flex-shrink: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ── File loader / spinner ───────────────────────────────── */
|
|
106
|
+
.dm-file-loader {
|
|
107
|
+
display: inline-flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
margin-left: 4px;
|
|
110
|
+
flex-shrink: 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.dm-spinner--xs {
|
|
114
|
+
width: 12px;
|
|
115
|
+
height: 12px;
|
|
116
|
+
border: 1.5px solid rgba(255, 255, 255, 0.1);
|
|
117
|
+
border-top-color: #f59e0b;
|
|
118
|
+
border-radius: 50%;
|
|
119
|
+
animation: dm-spin 0.7s linear infinite;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@keyframes dm-spin {
|
|
123
|
+
to {
|
|
124
|
+
transform: rotate(360deg);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* ── Progress bar ────────────────────────────────────────── */
|
|
129
|
+
.dm-progress-bar {
|
|
130
|
+
height: 2px;
|
|
131
|
+
background: rgba(255, 255, 255, 0.04);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.dm-progress-fill {
|
|
135
|
+
height: 100%;
|
|
136
|
+
background: #f59e0b;
|
|
137
|
+
transition: width 0.3s ease-out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.dm-progress-fill--passed {
|
|
141
|
+
background: #10b981;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.dm-progress-fill--failed {
|
|
145
|
+
background: #ef4444;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ── Expanded panel ──────────────────────────────────────── */
|
|
149
|
+
.dm-panel {
|
|
150
|
+
display: none;
|
|
151
|
+
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
152
|
+
height: 260px;
|
|
153
|
+
overflow: hidden;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.dm-panel--open {
|
|
157
|
+
display: flex;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ── Terminal (left pane) ────────────────────────────────── */
|
|
161
|
+
.dm-terminal {
|
|
162
|
+
flex: 1;
|
|
163
|
+
min-width: 0;
|
|
164
|
+
padding: 10px 14px;
|
|
165
|
+
overflow-y: auto;
|
|
166
|
+
background: #080d14;
|
|
167
|
+
font-family: "JetBrains Mono", "SF Mono", "Fira Code", monospace;
|
|
168
|
+
font-size: 11.5px;
|
|
169
|
+
line-height: 1.75;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.dm-terminal::-webkit-scrollbar {
|
|
173
|
+
width: 4px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.dm-terminal::-webkit-scrollbar-track {
|
|
177
|
+
background: transparent;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.dm-terminal::-webkit-scrollbar-thumb {
|
|
181
|
+
background: #2d3748;
|
|
182
|
+
border-radius: 2px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* Terminal line types */
|
|
186
|
+
.dm-term-line {
|
|
187
|
+
white-space: pre-wrap;
|
|
188
|
+
word-break: break-all;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.dm-term-line--command {
|
|
192
|
+
color: #4b5563;
|
|
193
|
+
font-size: 10.5px;
|
|
194
|
+
margin-bottom: 5px;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.dm-term-line--pass {
|
|
198
|
+
color: #4ade80;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.dm-term-line--fail {
|
|
202
|
+
color: #f87171;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.dm-term-line--error {
|
|
206
|
+
color: #f87171;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.dm-term-line--pending {
|
|
210
|
+
color: #fbbf24;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.dm-term-line--slow {
|
|
214
|
+
color: #fbbf24;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.dm-term-line--n1 {
|
|
218
|
+
color: #fb7185;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.dm-term-line--summary {
|
|
222
|
+
color: #e2e8f0;
|
|
223
|
+
font-weight: 600;
|
|
224
|
+
margin-top: 4px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.dm-term-line--info {
|
|
228
|
+
color: #374151;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.dm-term-separator {
|
|
232
|
+
border-top: 1px solid rgba(255, 255, 255, 0.07);
|
|
233
|
+
margin: 8px 0 4px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* ── Sidebar (right pane) ────────────────────────────────── */
|
|
237
|
+
.dm-sidebar {
|
|
238
|
+
width: 188px;
|
|
239
|
+
flex-shrink: 0;
|
|
240
|
+
border-left: 1px solid rgba(255, 255, 255, 0.06);
|
|
241
|
+
background: #060b12;
|
|
242
|
+
padding: 10px 10px 8px;
|
|
243
|
+
display: flex;
|
|
244
|
+
flex-direction: column;
|
|
245
|
+
gap: 6px;
|
|
246
|
+
overflow-y: auto;
|
|
247
|
+
font-size: 11px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.dm-sidebar::-webkit-scrollbar {
|
|
251
|
+
width: 3px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.dm-sidebar::-webkit-scrollbar-thumb {
|
|
255
|
+
background: #1e293b;
|
|
256
|
+
border-radius: 2px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* ── Stat card ───────────────────────────────────────────── */
|
|
260
|
+
.dm-sidebar-stat {
|
|
261
|
+
display: flex;
|
|
262
|
+
flex-direction: column;
|
|
263
|
+
gap: 3px;
|
|
264
|
+
background: rgba(255, 255, 255, 0.025);
|
|
265
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
266
|
+
border-radius: 6px;
|
|
267
|
+
padding: 7px 8px 6px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.dm-sidebar-stat-hd {
|
|
271
|
+
display: flex;
|
|
272
|
+
align-items: center;
|
|
273
|
+
gap: 6px;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.dm-sidebar-icon {
|
|
277
|
+
width: 14px;
|
|
278
|
+
height: 14px;
|
|
279
|
+
flex-shrink: 0;
|
|
280
|
+
opacity: 0.55;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.dm-sidebar-icon--slow {
|
|
284
|
+
color: #f59e0b;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.dm-sidebar-icon--n1 {
|
|
288
|
+
color: #fb7185;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.dm-sidebar-icon--cov {
|
|
292
|
+
color: #34d399;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.dm-sidebar-label {
|
|
296
|
+
flex: 1;
|
|
297
|
+
font-size: 9px;
|
|
298
|
+
font-weight: 700;
|
|
299
|
+
letter-spacing: 0.1em;
|
|
300
|
+
text-transform: uppercase;
|
|
301
|
+
color: #4b5563;
|
|
302
|
+
white-space: nowrap;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.dm-sidebar-value {
|
|
306
|
+
font-family: "JetBrains Mono", monospace;
|
|
307
|
+
font-size: 13px;
|
|
308
|
+
font-weight: 700;
|
|
309
|
+
color: #475569;
|
|
310
|
+
line-height: 1;
|
|
311
|
+
min-width: 20px;
|
|
312
|
+
text-align: right;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.dm-sidebar-value--slow {
|
|
316
|
+
color: #d97706;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.dm-sidebar-value--n1 {
|
|
320
|
+
color: #fb7185;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.dm-sidebar-value--cov {
|
|
324
|
+
color: #34d399;
|
|
325
|
+
font-size: 14px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* ── Item list (below stat header) ──────────────────────── */
|
|
329
|
+
.dm-sidebar-items {
|
|
330
|
+
display: flex;
|
|
331
|
+
flex-direction: column;
|
|
332
|
+
gap: 2px;
|
|
333
|
+
max-height: 52px;
|
|
334
|
+
overflow-y: auto;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.dm-sidebar-items:empty {
|
|
338
|
+
display: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.dm-sidebar-items::-webkit-scrollbar {
|
|
342
|
+
width: 2px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.dm-sidebar-items::-webkit-scrollbar-thumb {
|
|
346
|
+
background: #1e293b;
|
|
347
|
+
border-radius: 1px;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.dm-sidebar-item {
|
|
351
|
+
font-family: "JetBrains Mono", monospace;
|
|
352
|
+
font-size: 9.5px;
|
|
353
|
+
color: #374151;
|
|
354
|
+
line-height: 1.4;
|
|
355
|
+
word-break: break-all;
|
|
356
|
+
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
|
357
|
+
padding-top: 2px;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.dm-sidebar-item--slow {
|
|
361
|
+
color: #92400e;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.dm-sidebar-item--n1 {
|
|
365
|
+
color: #9f1239;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* ── Download log link ───────────────────────────────────── */
|
|
369
|
+
.dm-sidebar-log {
|
|
370
|
+
margin-top: auto;
|
|
371
|
+
padding-top: 4px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.dm-log-link {
|
|
375
|
+
display: none;
|
|
376
|
+
align-items: center;
|
|
377
|
+
gap: 5px;
|
|
378
|
+
color: #3b82f6;
|
|
379
|
+
font-size: 10px;
|
|
380
|
+
text-decoration: none;
|
|
381
|
+
opacity: 0.65;
|
|
382
|
+
transition: opacity 0.15s;
|
|
383
|
+
width: 100%;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.dm-log-link:hover {
|
|
387
|
+
opacity: 1;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* ── Header badges ───────────────────────────────────────── */
|
|
391
|
+
.dm-badge {
|
|
392
|
+
display: inline-flex;
|
|
393
|
+
align-items: center;
|
|
394
|
+
font-family: "JetBrains Mono", monospace;
|
|
395
|
+
font-size: 10px;
|
|
396
|
+
font-weight: 500;
|
|
397
|
+
padding: 2px 6px;
|
|
398
|
+
border-radius: 4px;
|
|
399
|
+
white-space: nowrap;
|
|
400
|
+
line-height: 16px;
|
|
401
|
+
letter-spacing: 0.01em;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.dm-badge--time {
|
|
405
|
+
background: rgba(255, 255, 255, 0.05);
|
|
406
|
+
color: #6b7280;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.dm-badge--n1 {
|
|
410
|
+
background: rgba(239, 68, 68, 0.12);
|
|
411
|
+
color: #f87171;
|
|
412
|
+
border: 1px solid rgba(239, 68, 68, 0.25);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.dm-badge--slow {
|
|
416
|
+
background: rgba(245, 158, 11, 0.12);
|
|
417
|
+
color: #fbbf24;
|
|
418
|
+
border: 1px solid rgba(245, 158, 11, 0.25);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.dm-badge--cov {
|
|
422
|
+
background: rgba(52, 211, 153, 0.12);
|
|
423
|
+
color: #34d399;
|
|
424
|
+
border: 1px solid rgba(52, 211, 153, 0.25);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/* ── Mode Indicator ────────────────────────────────────────── */
|
|
428
|
+
.dm-mode-indicator {
|
|
429
|
+
position: fixed;
|
|
430
|
+
bottom: 16px;
|
|
431
|
+
right: 16px;
|
|
432
|
+
background: #1a1a2e;
|
|
433
|
+
border: 1px solid #fbbf24;
|
|
434
|
+
color: #fbbf24;
|
|
435
|
+
padding: 8px 16px;
|
|
436
|
+
border-radius: 8px;
|
|
437
|
+
font-size: 12px;
|
|
438
|
+
font-weight: 600;
|
|
439
|
+
display: flex;
|
|
440
|
+
align-items: center;
|
|
441
|
+
gap: 10px;
|
|
442
|
+
z-index: 9999;
|
|
443
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.dm-mode-indicator--realtime {
|
|
447
|
+
background: #0a1929;
|
|
448
|
+
border-color: #10b981;
|
|
449
|
+
color: #10b981;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.dm-mode-dot {
|
|
453
|
+
width: 8px;
|
|
454
|
+
height: 8px;
|
|
455
|
+
border-radius: 50%;
|
|
456
|
+
background: currentColor;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.dm-mode-indicator--realtime .dm-mode-dot {
|
|
460
|
+
animation: dm-pulse 1.5s ease infinite;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.dm-retry-btn {
|
|
464
|
+
background: transparent;
|
|
465
|
+
border: 1px solid currentColor;
|
|
466
|
+
color: inherit;
|
|
467
|
+
padding: 4px 10px;
|
|
468
|
+
border-radius: 4px;
|
|
469
|
+
font-size: 11px;
|
|
470
|
+
cursor: pointer;
|
|
471
|
+
transition: background 0.2s;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.dm-retry-btn:hover {
|
|
475
|
+
background: rgba(255, 255, 255, 0.1);
|
|
476
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css.
|
|
3
|
+
*
|
|
4
|
+
* With Propshaft, assets are served efficiently without preprocessing steps. You can still include
|
|
5
|
+
* application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
|
|
6
|
+
* cascading order, meaning styles declared later in the document or manifest will override earlier ones,
|
|
7
|
+
* depending on specificity.
|
|
8
|
+
*
|
|
9
|
+
* Consider organizing styles into separate files for maintainability.
|
|
10
|
+
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Devformance
|
|
2
|
+
class MetricsChannel < ApplicationCable::Channel
|
|
3
|
+
def subscribed
|
|
4
|
+
case params[:stream_type]
|
|
5
|
+
when "run"
|
|
6
|
+
run_id = params[:run_id]
|
|
7
|
+
stream_from "devformance:run:#{run_id}" if run_id.present?
|
|
8
|
+
when "file"
|
|
9
|
+
file_key = params[:file_key]
|
|
10
|
+
run_id = params[:run_id]
|
|
11
|
+
if file_key.present? && run_id.present?
|
|
12
|
+
stream_from "devformance:file:#{file_key}:#{run_id}"
|
|
13
|
+
end
|
|
14
|
+
else
|
|
15
|
+
stream_from "devformance:metrics"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def unsubscribed
|
|
20
|
+
stop_all_streams
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
DevformanceChannel = MetricsChannel
|
|
25
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Devformance
|
|
2
|
+
class ApplicationController < ActionController::Base
|
|
3
|
+
helper ::Importmap::ImportmapTagsHelper if defined?(::Importmap::ImportmapTagsHelper)
|
|
4
|
+
helper ::Turbo::FramesHelper if defined?(::Turbo::FramesHelper)
|
|
5
|
+
|
|
6
|
+
allow_browser versions: :modern if respond_to?(:allow_browser)
|
|
7
|
+
protect_from_forgery with: :exception
|
|
8
|
+
|
|
9
|
+
def icon_svg_path
|
|
10
|
+
"/devformance/icon.svg"
|
|
11
|
+
end
|
|
12
|
+
helper_method :icon_svg_path
|
|
13
|
+
|
|
14
|
+
def icon_png_path
|
|
15
|
+
"/devformance/icon.png"
|
|
16
|
+
end
|
|
17
|
+
helper_method :icon_png_path
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Devformance
|
|
2
|
+
class IconsController < ApplicationController
|
|
3
|
+
skip_before_action :verify_authenticity_token
|
|
4
|
+
|
|
5
|
+
def svg
|
|
6
|
+
send_file(
|
|
7
|
+
Devformance::Engine.root.join("app/assets/images/icon.svg"),
|
|
8
|
+
type: "image/svg+xml",
|
|
9
|
+
disposition: "inline"
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def png
|
|
14
|
+
send_file(
|
|
15
|
+
Devformance::Engine.root.join("app/assets/images/icon.png"),
|
|
16
|
+
type: "image/png",
|
|
17
|
+
disposition: "inline"
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Devformance
|
|
2
|
+
class MetricsController < ApplicationController
|
|
3
|
+
skip_before_action :verify_authenticity_token, only: [ :run_tests, :run_status ]
|
|
4
|
+
|
|
5
|
+
def index
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def run_tests
|
|
9
|
+
result = ::Devformance::RunOrchestrator.call
|
|
10
|
+
if result[:error]
|
|
11
|
+
render json: { error: result[:error] }, status: :unprocessable_entity
|
|
12
|
+
else
|
|
13
|
+
render json: result, status: :accepted
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run_status
|
|
18
|
+
run = ::Devformance::Run.find_by(run_id: params[:run_id])
|
|
19
|
+
return render json: { error: "Not found" }, status: :not_found unless run
|
|
20
|
+
|
|
21
|
+
render json: {
|
|
22
|
+
run_id: run.run_id,
|
|
23
|
+
status: run.status,
|
|
24
|
+
files: run.file_results.map { |r|
|
|
25
|
+
{ file_key: r.file_key, file_path: r.file_path, status: r.status,
|
|
26
|
+
coverage: r.coverage, slow_query_count: r.slow_query_count, n1_count: r.n1_count }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def download_log
|
|
32
|
+
result = ::Devformance::FileResult.find_by(
|
|
33
|
+
run_id: params[:run_id], file_key: params[:file_key]
|
|
34
|
+
)
|
|
35
|
+
return render plain: "Not found", status: :not_found unless result&.log_path
|
|
36
|
+
return render plain: "Log not ready", status: :not_found unless File.exist?(result.log_path)
|
|
37
|
+
|
|
38
|
+
send_file result.log_path, type: "text/plain", disposition: "attachment"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module Devformance
|
|
2
|
+
class PlaygroundController < ApplicationController
|
|
3
|
+
def run
|
|
4
|
+
query_string = params[:query]
|
|
5
|
+
|
|
6
|
+
unless query_string.present?
|
|
7
|
+
return render json: { status: "error", output: "Empty query" }, status: :unprocessable_entity
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
result = nil
|
|
11
|
+
duration = 0
|
|
12
|
+
slow_queries_detected = []
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
# Enable bullet specifically for this block if possible, or track via notifications
|
|
16
|
+
ActiveSupport::Notifications.subscribe("sql.active_record") do |name, start, finish, id, payload|
|
|
17
|
+
# Only log SELECT/UPDATE/INSERT/DELETE that aren't schema
|
|
18
|
+
if payload[:sql] !~ /SCHEMA/
|
|
19
|
+
Rails.logger.debug "SQL: #{payload[:sql]}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Bullet.start_request if defined?(Bullet)
|
|
24
|
+
|
|
25
|
+
duration = Benchmark.ms do
|
|
26
|
+
# Evaluate safely (MVP risk assumed acceptable for demo playground)
|
|
27
|
+
# Using a restricted binding or at least catching standard errors
|
|
28
|
+
# In a real app we'd build an AST or not allow eval, but for this devformance demo it's requested
|
|
29
|
+
result = eval(query_string)
|
|
30
|
+
|
|
31
|
+
# Force execution if it's an ActiveRecord::Relation
|
|
32
|
+
result = result.to_a if result.is_a?(ActiveRecord::Relation)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if defined?(Bullet) && Bullet.notification_collector.notifications_present?
|
|
36
|
+
Bullet.notification_collector.collection.each do |notification|
|
|
37
|
+
next unless notification.is_a?(Bullet::Notification::NPlusOneQuery)
|
|
38
|
+
|
|
39
|
+
# We parse out the model and association
|
|
40
|
+
model = notification.base_class rescue "Unknown"
|
|
41
|
+
suggestion = notification.body
|
|
42
|
+
|
|
43
|
+
sq = Devformance::SlowQuery.create!(
|
|
44
|
+
model_class: model,
|
|
45
|
+
line_number: caller.first.match(/:(\d+):/)&.captures&.first&.to_i || 0,
|
|
46
|
+
fix_suggestion: suggestion
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
slow_queries_detected << sq
|
|
50
|
+
|
|
51
|
+
ActionCable.server.broadcast("devformance:metrics", {
|
|
52
|
+
type: "new_slow_query",
|
|
53
|
+
payload: {
|
|
54
|
+
id: sq.id,
|
|
55
|
+
model_class: sq.model_class,
|
|
56
|
+
line_number: sq.line_number,
|
|
57
|
+
fix_suggestion: sq.fix_suggestion,
|
|
58
|
+
duration: duration.round(2)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Bullet.end_request if defined?(Bullet)
|
|
65
|
+
|
|
66
|
+
QueryLog.create(query: query_string, duration: duration) rescue nil
|
|
67
|
+
|
|
68
|
+
render json: {
|
|
69
|
+
status: "success",
|
|
70
|
+
duration: duration.round(2),
|
|
71
|
+
output: result.inspect.truncate(1000)
|
|
72
|
+
}
|
|
73
|
+
rescue Exception => e
|
|
74
|
+
Bullet.end_request if defined?(Bullet)
|
|
75
|
+
# Also log failed queries
|
|
76
|
+
QueryLog.create(query: query_string, duration: 0) rescue nil
|
|
77
|
+
|
|
78
|
+
render json: {
|
|
79
|
+
status: "error",
|
|
80
|
+
duration: duration.round(2),
|
|
81
|
+
output: "#{e.class}: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
|
|
82
|
+
}
|
|
83
|
+
ensure
|
|
84
|
+
# Unsubscribe if we did
|
|
85
|
+
ActiveSupport::Notifications.unsubscribe("sql.active_record") rescue nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|