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.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +205 -0
  3. data/app/assets/builds/tailwind.css +2 -0
  4. data/app/assets/images/icon.png +0 -0
  5. data/app/assets/images/icon.svg +68 -0
  6. data/app/assets/stylesheets/devmetrics/dashboard.css +476 -0
  7. data/app/assets/stylesheets/devmetrics_live/application.css +10 -0
  8. data/app/assets/tailwind/application.css +1 -0
  9. data/app/channels/application_cable/channel.rb +4 -0
  10. data/app/channels/application_cable/connection.rb +4 -0
  11. data/app/channels/devformance/metrics_channel.rb +25 -0
  12. data/app/controllers/application_controller.rb +4 -0
  13. data/app/controllers/devformance/application_controller.rb +19 -0
  14. data/app/controllers/devformance/icons_controller.rb +21 -0
  15. data/app/controllers/devformance/metrics_controller.rb +41 -0
  16. data/app/controllers/devformance/playground_controller.rb +89 -0
  17. data/app/helpers/application_helper.rb +9 -0
  18. data/app/helpers/metrics_helper.rb +2 -0
  19. data/app/helpers/playground_helper.rb +2 -0
  20. data/app/javascript/devformance/channels/consumer.js +2 -0
  21. data/app/javascript/devformance/channels/index.js +1 -0
  22. data/app/javascript/devformance/controllers/application.js +9 -0
  23. data/app/javascript/devformance/controllers/hello_controller.js +7 -0
  24. data/app/javascript/devformance/controllers/index.js +14 -0
  25. data/app/javascript/devformance/controllers/metrics_controller.js +364 -0
  26. data/app/javascript/devformance/controllers/playground_controller.js +33 -0
  27. data/app/javascript/devmetrics.js +4 -0
  28. data/app/jobs/application_job.rb +7 -0
  29. data/app/jobs/devformance/file_runner_job.rb +318 -0
  30. data/app/mailers/application_mailer.rb +4 -0
  31. data/app/models/application_record.rb +3 -0
  32. data/app/models/devformance/file_result.rb +14 -0
  33. data/app/models/devformance/run.rb +19 -0
  34. data/app/models/devformance/slow_query.rb +5 -0
  35. data/app/views/devformance/metrics/index.html.erb +79 -0
  36. data/app/views/devformance/playground/run.html.erb +63 -0
  37. data/app/views/layouts/devformance/application.html.erb +856 -0
  38. data/app/views/layouts/mailer.html.erb +13 -0
  39. data/app/views/layouts/mailer.text.erb +1 -0
  40. data/app/views/metrics/index.html.erb +334 -0
  41. data/app/views/pwa/manifest.json.erb +22 -0
  42. data/app/views/pwa/service-worker.js +26 -0
  43. data/config/BUSINESS_LOGIC_PLAN.md +1244 -0
  44. data/config/application.rb +31 -0
  45. data/config/boot.rb +4 -0
  46. data/config/cable.yml +17 -0
  47. data/config/cache.yml +16 -0
  48. data/config/credentials.yml.enc +1 -0
  49. data/config/database.yml +98 -0
  50. data/config/deploy.yml +116 -0
  51. data/config/engine_routes.rb +13 -0
  52. data/config/environment.rb +5 -0
  53. data/config/environments/development.rb +84 -0
  54. data/config/environments/production.rb +90 -0
  55. data/config/environments/test.rb +59 -0
  56. data/config/importmap.rb +11 -0
  57. data/config/initializers/assets.rb +7 -0
  58. data/config/initializers/content_security_policy.rb +25 -0
  59. data/config/initializers/filter_parameter_logging.rb +8 -0
  60. data/config/initializers/inflections.rb +16 -0
  61. data/config/locales/en.yml +31 -0
  62. data/config/master.key +1 -0
  63. data/config/puma.rb +41 -0
  64. data/config/queue.yml +22 -0
  65. data/config/recurring.yml +15 -0
  66. data/config/routes.rb +20 -0
  67. data/config/storage.yml +34 -0
  68. data/db/migrate/20260317144616_create_slow_queries.rb +13 -0
  69. data/db/migrate/20260317175630_create_performance_runs.rb +14 -0
  70. data/db/migrate/20260317195043_add_run_id_to_slow_queries.rb +10 -0
  71. data/db/migrate/20260319000001_create_devformance_runs.rb +20 -0
  72. data/db/migrate/20260319000002_create_devformance_file_results.rb +29 -0
  73. data/db/migrate/20260319000003_add_columns_to_slow_queries.rb +7 -0
  74. data/lib/devformance/bullet_log_parser.rb +47 -0
  75. data/lib/devformance/compatibility.rb +12 -0
  76. data/lib/devformance/coverage_setup.rb +33 -0
  77. data/lib/devformance/engine.rb +80 -0
  78. data/lib/devformance/log_writer.rb +29 -0
  79. data/lib/devformance/run_orchestrator.rb +58 -0
  80. data/lib/devformance/sql_instrumentor.rb +29 -0
  81. data/lib/devformance/test_framework/base.rb +43 -0
  82. data/lib/devformance/test_framework/coverage_helper.rb +76 -0
  83. data/lib/devformance/test_framework/detector.rb +26 -0
  84. data/lib/devformance/test_framework/minitest.rb +71 -0
  85. data/lib/devformance/test_framework/registry.rb +24 -0
  86. data/lib/devformance/test_framework/rspec.rb +60 -0
  87. data/lib/devformance/test_helper.rb +42 -0
  88. data/lib/devformance/version.rb +3 -0
  89. data/lib/devformance.rb +196 -0
  90. data/lib/generators/devformance/install/install_generator.rb +73 -0
  91. data/lib/generators/devformance/install/templates/add_columns_to_slow_queries.rb.erb +7 -0
  92. data/lib/generators/devformance/install/templates/add_run_id_to_slow_queries.rb.erb +10 -0
  93. data/lib/generators/devformance/install/templates/create_devformance_file_results.rb.erb +29 -0
  94. data/lib/generators/devformance/install/templates/create_devformance_runs.rb.erb +20 -0
  95. data/lib/generators/devformance/install/templates/create_performance_runs.rb.erb +14 -0
  96. data/lib/generators/devformance/install/templates/create_slow_queries.rb.erb +13 -0
  97. data/lib/generators/devformance/install/templates/initializer.rb +23 -0
  98. data/lib/tasks/devformance.rake +45 -0
  99. data/spec/fixtures/devformance/devformance_run.rb +27 -0
  100. data/spec/fixtures/devformance/file_result.rb +34 -0
  101. data/spec/fixtures/devformance/slow_query.rb +11 -0
  102. data/spec/lib/devmetrics/log_writer_spec.rb +81 -0
  103. data/spec/lib/devmetrics/run_orchestrator_spec.rb +102 -0
  104. data/spec/lib/devmetrics/sql_instrumentor_spec.rb +115 -0
  105. data/spec/models/devmetrics/file_result_spec.rb +87 -0
  106. data/spec/models/devmetrics/run_spec.rb +66 -0
  107. data/spec/models/query_log_spec.rb +21 -0
  108. data/spec/rails_helper.rb +20 -0
  109. data/spec/requests/devmetrics/metrics_controller_spec.rb +149 -0
  110. data/spec/requests/devmetrics_pages_spec.rb +12 -0
  111. data/spec/requests/performance_spec.rb +17 -0
  112. data/spec/requests/slow_perf_spec.rb +9 -0
  113. data/spec/spec_helper.rb +114 -0
  114. data/spec/support/devmetrics_formatter.rb +106 -0
  115. data/spec/support/devmetrics_metrics.rb +37 -0
  116. data/spec/support/factory_bot.rb +3 -0
  117. metadata +200 -0
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5
+ <style>
6
+ /* Email styles need to be inline */
7
+ </style>
8
+ </head>
9
+
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1 @@
1
+ <%= yield %>
@@ -0,0 +1,334 @@
1
+ <%# ============================================================
2
+ Devformance — Dashboard View
3
+ Design: Dark technical, IBM Plex Sans + JetBrains Mono
4
+ Colors: #0F172A base, #1E293B cards, #22C55E accent
5
+ ============================================================ %>
6
+
7
+ <div data-controller="metrics">
8
+
9
+ <%# ── Page header ── %>
10
+ <div style="margin-bottom:2rem;">
11
+ <div style="display:flex; align-items:flex-end; justify-content:space-between; flex-wrap:wrap; gap:1rem;">
12
+ <div>
13
+ <p style="font-family:var(--font-mono); font-size:0.7rem; font-weight:600; letter-spacing:0.12em; color:var(--green); text-transform:uppercase; margin:0 0 0.375rem;">
14
+ Rails 8 · Solid Cable · No Redis
15
+ </p>
16
+ <h2 style="font-size:1.75rem; font-weight:700; color:var(--text-primary); margin:0; letter-spacing:-0.02em; line-height:1.2;">
17
+ Performance Dashboard
18
+ </h2>
19
+ <p style="color:var(--text-secondary); font-size:0.875rem; margin:0.375rem 0 0;">
20
+ Powered by Solid Cable — metrics broadcast on each test run.
21
+ </p>
22
+ </div>
23
+ <div style="display:flex; align-items:center; gap:0.5rem; font-size:0.75rem; color:var(--text-muted); font-family:var(--font-mono);">
24
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="color:#64748b;">
25
+ <circle cx="12" cy="12" r="10"/><polyline points="12,6 12,12 16,14"/>
26
+ </svg>
27
+ Last updated: <span id="last-updated-ts" style="color:var(--text-secondary);">just now</span>
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ <%# ── Run Performance Tests CTA ── %>
33
+ <div style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:1rem; background:rgba(34,197,94,0.06); border:1px solid rgba(34,197,94,0.2); border-radius:12px; padding:1.125rem 1.5rem; margin-bottom:1.75rem;">
34
+ <div style="display:flex; align-items:center; gap:1rem;">
35
+ <div style="display:flex; align-items:center; justify-content:center; width:40px; height:40px; border-radius:10px; background:rgba(34,197,94,0.12); border:1px solid rgba(34,197,94,0.25); flex-shrink:0;">
36
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#22C55E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="5,3 19,12 5,21"/></svg>
37
+ </div>
38
+ <div>
39
+ <p style="font-size:0.9375rem; font-weight:600; color:var(--text-primary); margin:0 0 0.125rem;">Run Performance Tests</p>
40
+ <p style="font-size:0.8125rem; color:var(--text-secondary); margin:0;">Scans <code style="font-family:var(--font-mono); color:#67E8F9;">spec/requests/</code> for request specs, instruments SQL, detects N+1s, and streams results live.</p>
41
+ </div>
42
+ </div>
43
+ <div style="display:flex; align-items:center; gap:0.75rem;">
44
+ <span style="font-size:0.7rem; color:var(--text-muted); font-family:var(--font-mono); white-space:nowrap;" data-metrics-target="testStatusBadge"></span>
45
+ <button
46
+ class="btn-primary"
47
+ data-action="click->metrics#runTests"
48
+ data-metrics-target="runTestsBtn"
49
+ id="run-tests-btn"
50
+ aria-label="Run performance test suite"
51
+ >
52
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="5,3 19,12 5,21"/></svg>
53
+ Run Performance Tests
54
+ </button>
55
+ </div>
56
+ </div>
57
+
58
+ <%# ── Live Terminal Output Panel (hidden until test run starts) ── %>
59
+ <div id="test-output-panel" data-metrics-target="testPanel" style="display:none; margin-bottom:1.75rem;">
60
+ <div class="panel">
61
+ <div class="panel-header">
62
+ <div style="display:flex; align-items:center; gap:0.625rem;">
63
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#4ADE80" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
64
+ <polyline points="4,17 10,11 4,5"/><line x1="12" y1="19" x2="20" y2="19"/>
65
+ </svg>
66
+ <h3 style="font-size:0.875rem; font-weight:600; color:var(--text-primary); margin:0; font-family:var(--font-mono);">Test Runner Output</h3>
67
+ </div>
68
+ <div style="display:flex; align-items:center; gap:0.75rem;">
69
+ <span class="badge badge-green" data-metrics-target="testStatus">streaming</span>
70
+ <button onclick="document.getElementById('test-output-panel').style.display='none'" style="background:none; border:none; color:var(--text-muted); cursor:pointer; font-size:1.1rem; line-height:1; padding:0;" aria-label="Close terminal output">✕</button>
71
+ </div>
72
+ </div>
73
+
74
+ <%# Progress Bar %>
75
+ <div style="padding: 0 1.5rem 1rem;">
76
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5rem;">
77
+ <span style="font-size:0.75rem; color:var(--text-secondary); font-family:var(--font-mono);" data-metrics-target="testProgressText">0% Complete</span>
78
+ <span style="font-size:0.75rem; color:var(--text-muted); font-family:var(--font-mono);" data-metrics-target="testFileCount">&nbsp;</span>
79
+ </div>
80
+ <div class="progress-track" style="height: 10px;">
81
+ <div class="progress-bar" style="width:0%; background:linear-gradient(90deg, #22C55E, #4ADE80);" data-metrics-target="testProgressBar"></div>
82
+ </div>
83
+ </div>
84
+
85
+ <div style="background:#030712; border-radius:0 0 12px 12px; padding:1rem 1.25rem; max-height:380px; overflow-y:auto;" id="test-terminal">
86
+ <pre style="margin:0; font-family:var(--font-mono); font-size:0.8125rem; line-height:1.6; white-space:pre-wrap; word-break:break-all;" data-metrics-target="testOutput"></pre>
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <%# ── KPI stat cards ── %>
92
+ <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:1rem; margin-bottom:1.75rem;">
93
+
94
+ <%# Total Queries %>
95
+ <div class="stat-card" aria-label="Total Queries stat">
96
+ <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:0.75rem;">
97
+ <span style="font-size:0.75rem; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; color:var(--text-muted);">Total Queries</span>
98
+ <span style="display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:8px; background:rgba(59,130,246,0.12); border:1px solid rgba(59,130,246,0.2);">
99
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#60A5FA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
100
+ <ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
101
+ </svg>
102
+ </span>
103
+ </div>
104
+ <div class="mono" style="font-size:2.25rem; font-weight:700; color:var(--text-primary); line-height:1; margin-bottom:0.375rem;" data-metrics-target="totalQueries">
105
+ <%= @total_queries %>
106
+ </div>
107
+ <div style="font-size:0.75rem; color:var(--text-muted);">all time</div>
108
+ </div>
109
+
110
+ <%# Avg Duration %>
111
+ <div class="stat-card" aria-label="Avg Query Duration stat">
112
+ <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:0.75rem;">
113
+ <span style="font-size:0.75rem; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; color:var(--text-muted);">Avg Duration</span>
114
+ <span style="display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:8px; background:rgba(234,179,8,0.12); border:1px solid rgba(234,179,8,0.2);">
115
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#FCD34D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
116
+ <polyline points="22,12 18,12 15,21 9,3 6,12 2,12"/>
117
+ </svg>
118
+ </span>
119
+ </div>
120
+ <div style="display:flex; align-items:baseline; gap:0.375rem;">
121
+ <div class="mono" style="font-size:2.25rem; font-weight:700; color:var(--text-primary); line-height:1; margin-bottom:0.375rem;" data-metrics-target="avgDuration">
122
+ <%= @avg_duration %>
123
+ </div>
124
+ <span style="font-family:var(--font-mono); font-size:0.875rem; color:var(--text-muted); margin-bottom:0.375rem;">ms</span>
125
+ </div>
126
+ <div style="font-size:0.75rem; color:var(--text-muted);">across logged queries</div>
127
+ </div>
128
+
129
+ <%# N+1 Detected %>
130
+ <div class="stat-card" aria-label="N+1 Issues stat">
131
+ <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:0.75rem;">
132
+ <span style="font-size:0.75rem; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; color:var(--text-muted);">N+1 Detected</span>
133
+ <span style="display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:8px; background:rgba(239,68,68,0.12); border:1px solid rgba(239,68,68,0.2);">
134
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#F87171" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
135
+ <path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/>
136
+ <line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
137
+ </svg>
138
+ </span>
139
+ </div>
140
+ <div class="mono" style="font-size:2.25rem; font-weight:700; color:<%= @n_plus_one_count > 0 ? '#EF4444' : '#22C55E' %>; line-height:1; margin-bottom:0.375rem;" data-metrics-target="nPlusOneCount">
141
+ <%= @n_plus_one_count %>
142
+ </div>
143
+ <div style="font-size:0.75rem; color:var(--text-muted);">via Bullet gem</div>
144
+ </div>
145
+
146
+ <%# Test Coverage %>
147
+ <div class="stat-card" aria-label="Test Coverage stat">
148
+ <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:0.75rem;">
149
+ <span style="font-size:0.75rem; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; color:var(--text-muted);">Test Coverage</span>
150
+ <span style="display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:8px; background:rgba(34,197,94,0.12); border:1px solid rgba(34,197,94,0.2);">
151
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#4ADE80" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
152
+ <polyline points="20,6 9,17 4,12"/>
153
+ </svg>
154
+ </span>
155
+ </div>
156
+ <div style="display:flex; align-items:baseline; gap:0.375rem;">
157
+ <div class="mono" style="font-size:2.25rem; font-weight:700; color:#22C55E; line-height:1; margin-bottom:0.375rem;" data-metrics-target="coverage">
158
+ 92.3%
159
+ </div>
160
+ <span data-metrics-target="coverageTrend" style="font-size:0.7rem; color:#4ADE80; font-family:var(--font-mono); display:flex; align-items:center; gap:0.125rem; margin-bottom:0.375rem;">
161
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="18,15 12,9 6,15"/></svg>
162
+ live
163
+ </span>
164
+ </div>
165
+ <div style="font-size:0.75rem; color:var(--text-muted);">SimpleCov · updating live</div>
166
+ </div>
167
+
168
+ </div>
169
+
170
+ <%# ── Bottom grid: Slow Queries + Right Column ── %>
171
+ <div style="display:grid; grid-template-columns:1fr 380px; gap:1.5rem; align-items:start;" id="main-grid">
172
+
173
+ <%# Left: Slow Queries Panel %>
174
+ <div class="panel">
175
+ <div class="panel-header">
176
+ <div style="display:flex; align-items:center; gap:0.625rem;">
177
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#F87171" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
178
+ <path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/>
179
+ <line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
180
+ </svg>
181
+ <h3 style="font-size:0.9375rem; font-weight:600; color:var(--text-primary); margin:0;">N+1 & Slow Queries</h3>
182
+ </div>
183
+ <span class="badge badge-red">
184
+ <span class="pulse-dot" style="width:5px;height:5px;"></span>
185
+ Tracking
186
+ </span>
187
+ </div>
188
+
189
+ <div style="padding:1.25rem 1.5rem;" id="slow-queries-list" data-metrics-target="slowQueriesList">
190
+ <% if @recent_slow_queries.any? %>
191
+ <div style="display:flex; flex-direction:column; gap:0.875rem;">
192
+ <% @recent_slow_queries.each do |query| %>
193
+ <div class="slow-query-item fade-in">
194
+ <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:0.5rem;">
195
+ <div style="display:flex; align-items:center; gap:0.5rem;">
196
+ <span class="badge badge-red">N+1</span>
197
+ <span style="font-size:0.875rem; font-weight:600; color:#FCA5A5; font-family:var(--font-mono);"><%= query.model_class %></span>
198
+ </div>
199
+ <span style="font-size:0.7rem; color:var(--text-muted); font-family:var(--font-mono);">line <%= query.line_number %></span>
200
+ </div>
201
+ <div style="background:rgba(15,23,42,0.8); border:1px solid rgba(239,68,68,0.15); border-radius:6px; padding:0.625rem 0.75rem;">
202
+ <span style="font-size:0.75rem; color:var(--text-muted); display:block; margin-bottom:0.25rem; letter-spacing:0.04em; text-transform:uppercase; font-weight:600;">Fix suggestion</span>
203
+ <code style="font-family:var(--font-mono); font-size:0.8rem; color:#A5F3FC; word-break:break-all;"><%= query.fix_suggestion %></code>
204
+ </div>
205
+ <div style="margin-top:0.5rem; font-size:0.7rem; color:var(--text-muted); font-family:var(--font-mono);">
206
+ Detected <%= time_ago_in_words(query.created_at) %> ago
207
+ </div>
208
+ </div>
209
+ <% end %>
210
+ </div>
211
+ <% else %>
212
+ <div id="no-slow-queries-msg" style="text-align:center; padding:3rem 1rem;">
213
+ <div style="display:inline-flex; align-items:center; justify-content:center; width:56px; height:56px; border-radius:14px; background:rgba(34,197,94,0.1); border:1px solid rgba(34,197,94,0.2); margin-bottom:1rem;">
214
+ <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#4ADE80" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
215
+ <path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22,4 12,14.01 9,11.01"/>
216
+ </svg>
217
+ </div>
218
+ <p style="font-size:0.9375rem; font-weight:600; color:var(--text-primary); margin:0 0 0.375rem;">All clear!</p>
219
+ <p style="font-size:0.8125rem; color:var(--text-muted); margin:0;">No N+1s or slow queries detected. Try running queries in the Playground.</p>
220
+ </div>
221
+ <% end %>
222
+ </div>
223
+ </div>
224
+
225
+ <%# Right column %>
226
+ <div style="display:flex; flex-direction:column; gap:1rem;">
227
+
228
+ <%# Playground CTA — dark variant with green glow %>
229
+ <div style="
230
+ background: linear-gradient(135deg, rgba(34,197,94,0.12) 0%, rgba(16,185,129,0.06) 100%);
231
+ border: 1px solid rgba(34,197,94,0.25);
232
+ border-radius: 12px;
233
+ padding: 1.5rem;
234
+ text-align: center;
235
+ cursor: pointer;
236
+ transition: border-color 0.2s, box-shadow 0.2s;
237
+ " onmouseover="this.style.borderColor='rgba(34,197,94,0.5)'; this.style.boxShadow='0 0 28px rgba(34,197,94,0.12)';" onmouseout="this.style.borderColor='rgba(34,197,94,0.25)'; this.style.boxShadow='none';">
238
+ <div style="display:inline-flex; align-items:center; justify-content:center; width:44px; height:44px; border-radius:10px; background:rgba(34,197,94,0.15); border:1px solid rgba(34,197,94,0.3); margin-bottom:0.875rem;">
239
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#22C55E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
240
+ <polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>
241
+ </svg>
242
+ </div>
243
+ <h3 style="font-size:1rem; font-weight:700; color:var(--text-primary); margin:0 0 0.375rem; letter-spacing:-0.01em;">Query Playground</h3>
244
+ <p style="font-size:0.8125rem; color:var(--text-secondary); margin:0 0 1.125rem; line-height:1.5;">Execute ActiveRecord queries & watch metrics update live via Solid Cable broadcast.</p>
245
+ <a href="/playground/run" class="btn-primary" style="width:100%; justify-content:center;">
246
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
247
+ <polygon points="5,3 19,12 5,21"/>
248
+ </svg>
249
+ Launch Playground
250
+ </a>
251
+ </div>
252
+
253
+ <%# System Resources %>
254
+ <div class="panel">
255
+ <div class="panel-header">
256
+ <div style="display:flex; align-items:center; gap:0.625rem;">
257
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#A78BFA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
258
+ <rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>
259
+ </svg>
260
+ <h3 style="font-size:0.875rem; font-weight:600; color:var(--text-primary); margin:0;">System Resources</h3>
261
+ </div>
262
+ <span class="badge badge-muted">Live</span>
263
+ </div>
264
+ <div style="padding:1.25rem 1.5rem; display:flex; flex-direction:column; gap:1.25rem;">
265
+
266
+ <%# Memory %>
267
+ <div>
268
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5rem;">
269
+ <span style="display:flex; align-items:center; gap:0.375rem; font-size:0.8125rem; color:var(--text-secondary);">
270
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#60A5FA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
271
+ <path d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/>
272
+ </svg>
273
+ Memory
274
+ </span>
275
+ <span class="mono" style="font-size:0.8125rem; color:var(--text-primary);" data-metrics-target="memoryUsage">245 MB</span>
276
+ </div>
277
+ <div class="progress-track">
278
+ <div class="progress-bar" style="width:45%; background:linear-gradient(90deg, #3B82F6, #60A5FA);" data-metrics-target="memoryBar"></div>
279
+ </div>
280
+ </div>
281
+
282
+ <%# DB Connections %>
283
+ <div>
284
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5rem;">
285
+ <span style="display:flex; align-items:center; gap:0.375rem; font-size:0.8125rem; color:var(--text-secondary);">
286
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#A78BFA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
287
+ <path d="M13 10V3L4 14h7v7l9-11h-7z"/>
288
+ </svg>
289
+ DB Connections
290
+ </span>
291
+ <span class="mono" style="font-size:0.8125rem; color:var(--text-primary);" data-metrics-target="dbConnections">14 / 50</span>
292
+ </div>
293
+ <div class="progress-track">
294
+ <div class="progress-bar" style="width:28%; background:linear-gradient(90deg, #A855F7, #C084FC);" data-metrics-target="connectionBar"></div>
295
+ </div>
296
+ </div>
297
+
298
+ <%# Coverage bar %>
299
+ <div>
300
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5rem;">
301
+ <span style="display:flex; align-items:center; gap:0.375rem; font-size:0.8125rem; color:var(--text-secondary);">
302
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#4ADE80" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
303
+ <polyline points="20,6 9,17 4,12"/>
304
+ </svg>
305
+ Test Coverage
306
+ </span>
307
+ <span class="mono" style="font-size:0.8125rem; color:#22C55E;" data-metrics-target="coverageBar">92.3%</span>
308
+ </div>
309
+ <div class="progress-track">
310
+ <div class="progress-bar" style="width:92.3%; background:linear-gradient(90deg, #16A34A, #22C55E);" data-metrics-target="coverageBarEl"></div>
311
+ </div>
312
+ </div>
313
+
314
+ </div>
315
+ </div>
316
+ </div>
317
+ </div>
318
+
319
+ </div>
320
+
321
+ <%# Responsive grid collapse %>
322
+ <style>
323
+ @media (max-width: 900px) {
324
+ #main-grid { grid-template-columns: 1fr !important; }
325
+ }
326
+ </style>
327
+
328
+ <script>
329
+ // Update the "last updated" timestamp on every cable broadcast
330
+ document.addEventListener('metrics:updated', () => {
331
+ const el = document.getElementById('last-updated-ts')
332
+ if (el) el.textContent = new Date().toLocaleTimeString()
333
+ })
334
+ </script>
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "Devformance",
3
+ "icons": [
4
+ {
5
+ "src": "/icon.png",
6
+ "type": "image/png",
7
+ "sizes": "512x512"
8
+ },
9
+ {
10
+ "src": "/icon.png",
11
+ "type": "image/png",
12
+ "sizes": "512x512",
13
+ "purpose": "maskable"
14
+ }
15
+ ],
16
+ "start_url": "/",
17
+ "display": "standalone",
18
+ "scope": "/",
19
+ "description": "Devformance.",
20
+ "theme_color": "red",
21
+ "background_color": "red"
22
+ }
@@ -0,0 +1,26 @@
1
+ // Add a service worker for processing Web Push notifications:
2
+ //
3
+ // self.addEventListener("push", async (event) => {
4
+ // const { title, options } = await event.data.json()
5
+ // event.waitUntil(self.registration.showNotification(title, options))
6
+ // })
7
+ //
8
+ // self.addEventListener("notificationclick", function(event) {
9
+ // event.notification.close()
10
+ // event.waitUntil(
11
+ // clients.matchAll({ type: "window" }).then((clientList) => {
12
+ // for (let i = 0; i < clientList.length; i++) {
13
+ // let client = clientList[i]
14
+ // let clientPath = (new URL(client.url)).pathname
15
+ //
16
+ // if (clientPath == event.notification.data.path && "focus" in client) {
17
+ // return client.focus()
18
+ // }
19
+ // }
20
+ //
21
+ // if (clients.openWindow) {
22
+ // return clients.openWindow(event.notification.data.path)
23
+ // }
24
+ // })
25
+ // )
26
+ // })