solidstats 1.1.0 → 3.0.0.beta.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +27 -0
  4. data/Rakefile +3 -3
  5. data/app/assets/stylesheets/solidstats/dashboard.css +48 -0
  6. data/app/controllers/solidstats/dashboard_controller.rb +82 -60
  7. data/app/controllers/solidstats/logs_controller.rb +72 -0
  8. data/app/controllers/solidstats/performance_controller.rb +25 -0
  9. data/app/controllers/solidstats/productivity_controller.rb +39 -0
  10. data/app/controllers/solidstats/quality_controller.rb +152 -0
  11. data/app/controllers/solidstats/securities_controller.rb +30 -0
  12. data/app/helpers/solidstats/application_helper.rb +155 -0
  13. data/app/helpers/solidstats/performance_helper.rb +87 -0
  14. data/app/helpers/solidstats/productivity_helper.rb +38 -0
  15. data/app/services/solidstats/bundler_audit_service.rb +206 -0
  16. data/app/services/solidstats/coverage_compass_service.rb +335 -0
  17. data/app/services/solidstats/load_lens_service.rb +454 -0
  18. data/app/services/solidstats/log_size_monitor_service.rb +205 -74
  19. data/app/services/solidstats/my_todo_service.rb +242 -0
  20. data/app/services/solidstats/style_patrol_service.rb +319 -0
  21. data/app/views/layouts/solidstats/application.html.erb +9 -2
  22. data/app/views/layouts/solidstats/dashboard.html.erb +84 -0
  23. data/app/views/solidstats/dashboard/dashboard.html.erb +39 -0
  24. data/app/views/solidstats/logs/logs_size.html.erb +409 -0
  25. data/app/views/solidstats/performance/load_lens.html.erb +158 -0
  26. data/app/views/solidstats/productivity/_todo_list.html.erb +49 -0
  27. data/app/views/solidstats/productivity/my_todos.html.erb +84 -0
  28. data/app/views/solidstats/quality/coverage_compass.html.erb +420 -0
  29. data/app/views/solidstats/quality/style_patrol.html.erb +463 -0
  30. data/app/views/solidstats/securities/bundler_audit.html.erb +345 -0
  31. data/app/views/solidstats/shared/_dashboard_card.html.erb +160 -0
  32. data/app/views/solidstats/shared/_quick_actions.html.erb +26 -0
  33. data/config/routes.rb +32 -4
  34. data/lib/generators/solidstats/install/install_generator.rb +28 -2
  35. data/lib/generators/solidstats/install/templates/README +7 -0
  36. data/lib/solidstats/version.rb +1 -1
  37. data/lib/tasks/solidstats_performance.rake +84 -0
  38. metadata +43 -19
  39. data/app/services/solidstats/audit_service.rb +0 -56
  40. data/app/services/solidstats/data_collector_service.rb +0 -83
  41. data/app/services/solidstats/todo_service.rb +0 -114
  42. data/app/views/solidstats/dashboard/_log_monitor.html.erb +0 -759
  43. data/app/views/solidstats/dashboard/_todos.html.erb +0 -151
  44. data/app/views/solidstats/dashboard/audit/_additional_styles.css +0 -22
  45. data/app/views/solidstats/dashboard/audit/_audit_badge.html.erb +0 -5
  46. data/app/views/solidstats/dashboard/audit/_audit_details.html.erb +0 -495
  47. data/app/views/solidstats/dashboard/audit/_audit_summary.html.erb +0 -26
  48. data/app/views/solidstats/dashboard/audit/_no_vulnerabilities.html.erb +0 -3
  49. data/app/views/solidstats/dashboard/audit/_security_audit.html.erb +0 -14
  50. data/app/views/solidstats/dashboard/audit/_vulnerabilities_table.html.erb +0 -1120
  51. data/app/views/solidstats/dashboard/audit/_vulnerability_details.html.erb +0 -63
  52. data/app/views/solidstats/dashboard/index.html.erb +0 -1351
  53. data/lib/tasks/solidstats_tasks.rake +0 -4
@@ -0,0 +1,409 @@
1
+ <%#
2
+ Log Size Monitoring Page - DaisyUI Version
3
+ Shows all log files with sizes and provides truncation functionality
4
+ %>
5
+
6
+ <div class="dashboard-enter">
7
+ <!-- Breadcrumb Navigation -->
8
+ <div class="breadcrumbs text-sm mb-6">
9
+ <ul>
10
+ <li>
11
+ <%= link_to solidstats.solidstats_dashboard_path, class: "link link-hover" do %>
12
+ <i data-feather="home" class="w-4 h-4 mr-1"></i>
13
+ Dashboard
14
+ <% end %>
15
+ </li>
16
+ <li>Log Files</li>
17
+ </ul>
18
+ </div>
19
+
20
+ <!-- Page Header -->
21
+ <div class="hero bg-base-200 rounded-xl mb-8">
22
+ <div class="hero-content text-center py-8">
23
+ <div class="max-w-2xl">
24
+ <h1 class="text-4xl font-bold mb-4">Log Files Monitoring</h1>
25
+ <p class="text-base-content/70 mb-6">
26
+ Monitor and manage your application log files with real-time size tracking
27
+ </p>
28
+
29
+ <!-- Action Buttons -->
30
+ <div class="flex flex-wrap justify-center gap-3">
31
+ <button onclick="refreshLogs()" class="btn btn-primary">
32
+ <i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i>
33
+ Refresh Data
34
+ </button>
35
+ <button onclick="downloadLogsData()" class="btn btn-outline">
36
+ <i data-feather="download" class="w-4 h-4 mr-2"></i>
37
+ Export Data
38
+ </button>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <% if @logs_data.present? %>
45
+ <!-- Summary Cards using Dashboard Card Style -->
46
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
47
+ <!-- Total Files Card -->
48
+ <div class="group relative">
49
+ <div class="card-container bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 shadow-lg hover:shadow-xl rounded-xl backdrop-blur-md transition-all duration-300">
50
+ <div class="absolute inset-0 bg-gradient-to-r from-blue-500 to-indigo-500 rounded-2xl opacity-0 group-hover:opacity-20 transition-opacity duration-300"></div>
51
+ <div class="relative p-4">
52
+ <div class="flex items-center justify-between mb-3">
53
+ <div class="icon-container w-10 h-10 rounded-xl bg-gradient-to-r from-blue-500 to-indigo-500 flex items-center justify-center shadow-lg transform group-hover:scale-110 transition-transform duration-200">
54
+ <i data-feather="file-text" class="w-5 h-5 text-white"></i>
55
+ </div>
56
+ <div class="w-2 h-2 rounded-full bg-gradient-to-r from-blue-500 to-indigo-500 animate-pulse"></div>
57
+ </div>
58
+ <div class="text-2xl font-bold text-slate-900 dark:text-slate-100 group-hover:text-transparent group-hover:bg-gradient-to-r group-hover:from-blue-500 group-hover:to-indigo-500 group-hover:bg-clip-text transition-all duration-300 mb-1">
59
+ <%= @logs_data.dig("summary", "total_files") %>
60
+ </div>
61
+ <h3 class="text-md font-semibold text-slate-800 dark:text-slate-200 mb-2">Total Files</h3>
62
+ <div class="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-blue-500 to-indigo-500 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-left"></div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ <!-- Total Size Card -->
68
+ <div class="group relative">
69
+ <div class="card-container bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 shadow-lg hover:shadow-xl rounded-xl backdrop-blur-md transition-all duration-300">
70
+ <div class="absolute inset-0 bg-gradient-to-r from-purple-500 to-pink-500 rounded-2xl opacity-0 group-hover:opacity-20 transition-opacity duration-300"></div>
71
+ <div class="relative p-4">
72
+ <div class="flex items-center justify-between mb-3">
73
+ <div class="icon-container w-10 h-10 rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center shadow-lg transform group-hover:scale-110 transition-transform duration-200">
74
+ <i data-feather="hard-drive" class="w-5 h-5 text-white"></i>
75
+ </div>
76
+ <div class="w-2 h-2 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 animate-pulse"></div>
77
+ </div>
78
+ <div class="text-2xl font-bold text-slate-900 dark:text-slate-100 group-hover:text-transparent group-hover:bg-gradient-to-r group-hover:from-purple-500 group-hover:to-pink-500 group-hover:bg-clip-text transition-all duration-300 mb-1">
79
+ <%= @logs_data.dig("summary", "total_size") %>
80
+ </div>
81
+ <h3 class="text-md font-semibold text-slate-800 dark:text-slate-200 mb-2">Total Size</h3>
82
+ <div class="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-purple-500 to-pink-500 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-left"></div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <!-- Largest File Card -->
88
+ <div class="group relative">
89
+ <div class="card-container bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 shadow-lg hover:shadow-xl rounded-xl backdrop-blur-md transition-all duration-300">
90
+ <div class="absolute inset-0 bg-gradient-to-r from-orange-500 to-red-500 rounded-2xl opacity-0 group-hover:opacity-20 transition-opacity duration-300"></div>
91
+ <div class="relative p-4">
92
+ <div class="flex items-center justify-between mb-3">
93
+ <div class="icon-container w-10 h-10 rounded-xl bg-gradient-to-r from-orange-500 to-red-500 flex items-center justify-center shadow-lg transform group-hover:scale-110 transition-transform duration-200">
94
+ <i data-feather="trending-up" class="w-5 h-5 text-white"></i>
95
+ </div>
96
+ <div class="w-2 h-2 rounded-full bg-gradient-to-r from-orange-500 to-red-500 animate-pulse"></div>
97
+ </div>
98
+ <div class="text-xl font-bold text-slate-900 dark:text-slate-100 group-hover:text-transparent group-hover:bg-gradient-to-r group-hover:from-orange-500 group-hover:to-red-500 group-hover:bg-clip-text transition-all duration-300 mb-1">
99
+ <%= @logs_data.dig("summary", "largest_file") %>
100
+ </div>
101
+ <div class="text-sm text-slate-500 dark:text-slate-400 mb-2"><%= @logs_data.dig("summary", "largest_file_size") %></div>
102
+ <h3 class="text-md font-semibold text-slate-800 dark:text-slate-200 mb-2">Largest File</h3>
103
+ <div class="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-orange-500 to-red-500 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-left"></div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- Status Card -->
109
+ <div class="group relative">
110
+ <%
111
+ status = @logs_data.dig('summary', 'status')
112
+ status_colors = {
113
+ 'success' => { bg: 'bg-emerald-50 dark:bg-emerald-900/20', border: 'border-emerald-200 dark:border-emerald-800', gradient: 'from-emerald-500 to-green-500' },
114
+ 'warning' => { bg: 'bg-amber-50 dark:bg-amber-900/20', border: 'border-amber-200 dark:border-amber-800', gradient: 'from-amber-500 to-orange-500' },
115
+ 'error' => { bg: 'bg-red-50 dark:bg-red-900/20', border: 'border-red-200 dark:border-red-800', gradient: 'from-red-500 to-rose-500' }
116
+ }
117
+ current_status = status_colors[status] || status_colors['success']
118
+ %>
119
+ <div class="card-container <%= current_status[:bg] %> border <%= current_status[:border] %> shadow-lg hover:shadow-xl rounded-xl backdrop-blur-md transition-all duration-300">
120
+ <div class="absolute inset-0 bg-gradient-to-r <%= current_status[:gradient] %> rounded-2xl opacity-0 group-hover:opacity-20 transition-opacity duration-300"></div>
121
+ <div class="relative p-4">
122
+ <div class="flex items-center justify-between mb-3">
123
+ <div class="icon-container w-10 h-10 rounded-xl bg-gradient-to-r <%= current_status[:gradient] %> flex items-center justify-center shadow-lg transform group-hover:scale-110 transition-transform duration-200">
124
+ <i data-feather="<%= status == 'success' ? 'check-circle' : 'alert-circle' %>" class="w-5 h-5 text-white"></i>
125
+ </div>
126
+ <div class="w-2 h-2 rounded-full bg-gradient-to-r <%= current_status[:gradient] %> animate-pulse"></div>
127
+ </div>
128
+ <div class="text-2xl font-bold text-slate-900 dark:text-slate-100 group-hover:text-transparent group_hover:<%= current_status[:gradient] %> group-hover:bg-clip-text transition-all duration-300 mb-1 capitalize">
129
+ <%= status %>
130
+ </div>
131
+ <h3 class="text-md font-semibold text-slate-800 dark:text-slate-200 mb-2">System Status</h3>
132
+ <div class="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r <%= current_status[:gradient] %> transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-left"></div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ <!-- Log Files Table -->
139
+ <div class="group relative mb-8">
140
+ <div class="card-container bg-white/80 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 shadow-lg hover:shadow-xl rounded-xl backdrop-blur-md transition-all duration-300 overflow-hidden">
141
+ <div class="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl opacity-0 group-hover:opacity-5 transition-opacity duration-300"></div>
142
+ <!-- Table Header -->
143
+ <div class="relative p-6 border-b border-slate-200 dark:border-slate-700">
144
+ <div class="flex flex-col lg:flex-row lg:justify-between lg:items-center space-y-4 lg:space-y-0">
145
+ <div>
146
+ <h3 class="text-xl font-bold text-slate-900 dark:text-slate-100 mb-1">Log Files</h3>
147
+ <p class="text-sm text-slate-500 dark:text-slate-400">Manage your application log files and monitor disk usage</p>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ <% if @logs_data["files"].present? %>
152
+ <div class="relative overflow-x-auto px-0">
153
+ <table class="w-full min-w-full text-left table-auto divide-y divide-slate-700">
154
+ <thead class="bg-slate-900/70 border-b border-slate-800">
155
+ <tr>
156
+ <th class="px-4 py-3 text-left text-xs font-bold text-slate-400 uppercase tracking-wider">File Name</th>
157
+ <th class="px-4 py-3 text-left text-xs font-bold text-slate-400 uppercase tracking-wider">Size</th>
158
+ <th class="px-4 py-3 text-left text-xs font-bold text-slate-400 uppercase tracking-wider">Last Modified</th>
159
+ <th class="px-4 py-3 text-left text-xs font-bold text-slate-400 uppercase tracking-wider">Status</th>
160
+ <th class="px-4 py-3 text-right text-xs font-bold text-slate-400 uppercase tracking-wider">Actions</th>
161
+ </tr>
162
+ </thead>
163
+ <tbody class="bg-slate-800/80 divide-y divide-slate-700">
164
+ <% @logs_data["files"].each do |file| %>
165
+ <tr class="hover:bg-slate-700/50 transition">
166
+ <!-- File Name + Path -->
167
+ <td class="py-4 px-4 align-middle">
168
+ <div class="flex items-center space-x-3">
169
+ <span class="flex-shrink-0 w-5 h-5 text-blue-400"><i data-feather="file-text"></i></span>
170
+ <div>
171
+ <div class="font-semibold text-white"><%= file["name"] %></div>
172
+ <div class="text-xs text-slate-400 truncate max-w-xs" title="<%= file["path"] %>">
173
+ <%= file["path"] %>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </td>
178
+ <!-- Size -->
179
+ <td class="py-4 px-4 align-middle">
180
+ <span class="font-semibold text-slate-200"><%= file["size_human"] %></span>
181
+ <div class="text-xs text-slate-500"><%= number_with_delimiter(file["size_bytes"]) %> bytes</div>
182
+ </td>
183
+ <!-- Last Modified -->
184
+ <td class="py-4 px-4 align-middle">
185
+ <span class="inline-flex items-center gap-1 text-slate-300">
186
+ <i data-feather="clock" class="w-4 h-4"></i>
187
+ <%= time_ago_in_words(Time.parse(file["last_modified"])) %> ago
188
+ </span>
189
+ </td>
190
+ <!-- Status -->
191
+ <td class="py-4 px-4 align-middle">
192
+ <span class="inline-flex items-center px-2 py-1 bg-green-600 text-white text-xs rounded-full gap-1">
193
+ <i data-feather="check-circle" class="w-3 h-3"></i>
194
+ Success
195
+ </span>
196
+ </td>
197
+ <!-- Actions -->
198
+ <td class="py-4 px-4 align-middle text-right">
199
+ <button
200
+ class="inline-flex items-center px-3 py-1 text-xs font-semibold rounded bg-slate-700 hover:bg-red-600 text-slate-200 hover:text-white border border-transparent hover:border-red-800 transition"
201
+ onclick="truncateLog('<%= file["name"] %>')"
202
+ title="Truncate log"
203
+ >
204
+ <i data-feather="scissors" class="w-4 h-4 mr-1"></i>
205
+ Truncate
206
+ </button>
207
+ </td>
208
+ </tr>
209
+ <% end %>
210
+ </tbody>
211
+ </table>
212
+ </div>
213
+ <% else %>
214
+ <!-- Empty State -->
215
+ <div class="relative px-6 py-16 text-center">
216
+ <div class="w-20 h-20 bg-gradient-to-r from-slate-200 to-slate-300 dark:from-slate-700 dark:to-slate-600 rounded-full flex items-center justify-center mx-auto mb-6">
217
+ <i data-feather="file-text" class="w-10 h-10 text-slate-400 dark:text-slate-500"></i>
218
+ </div>
219
+ <h4 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">No log files found</h4>
220
+ <p class="text-slate-500 dark:text-slate-400 max-w-sm mx-auto">
221
+ There are currently no log files to display. Check back later or refresh the data.
222
+ </p>
223
+ </div>
224
+ <% end %>
225
+ </div>
226
+ </div>
227
+ <% else %>
228
+ <!-- Error State -->
229
+ <div class="text-center py-16">
230
+ <div class="w-24 h-24 bg-gradient-to-r from-red-200 to-orange-200 dark:from-red-900/30 dark:to-orange-900/30 rounded-full flex items-center justify-center mx-auto mb-6">
231
+ <i data-feather="alert-circle" class="w-12 h-12 text-red-500 dark:text-red-400"></i>
232
+ </div>
233
+ <h3 class="text-xl font-bold text-slate-900 dark:text-slate-100 mb-3">Unable to Load Log Data</h3>
234
+ <p class="text-slate-500 dark:text-slate-400 mb-6 max-w-md mx-auto">
235
+ There was an error loading the log monitoring data. Please try refreshing the data or check back later.
236
+ </p>
237
+ <button onclick="refreshLogs()"
238
+ class="inline-flex items-center px-6 py-3 rounded-xl text-sm font-medium text-white bg-gradient-to-r from-blue-500 to-indigo-500 hover:from-blue-600 hover:to-indigo-600 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105">
239
+ <i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i>
240
+ Try Again
241
+ </button>
242
+ </div>
243
+ <% end %>
244
+ </div>
245
+
246
+ <!-- Enhanced Toast Notification -->
247
+ <div id="toast" class="fixed top-4 right-4 z-50 hidden">
248
+ <div class="bg-white/90 dark:bg-slate-800/90 backdrop-blur-lg border border-slate-200 dark:border-slate-700 rounded-xl shadow-2xl p-4 max-w-sm transform transition-all duration-300">
249
+ <div class="flex items-center">
250
+ <div class="flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center mr-3">
251
+ <i id="toast-icon" data-feather="check-circle" class="w-5 h-5"></i>
252
+ </div>
253
+ <div class="flex-1">
254
+ <p id="toast-message" class="text-sm font-medium text-slate-900 dark:text-slate-100"></p>
255
+ </div>
256
+ <div class="ml-3">
257
+ <button onclick="hideToast()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors duration-200">
258
+ <i data-feather="x" class="w-4 h-4"></i>
259
+ </button>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ </div>
264
+
265
+ <script>
266
+ // Initialize feather icons
267
+ feather.replace();
268
+
269
+ function refreshLogs() {
270
+ showToast('Refreshing log data...', 'info');
271
+
272
+ fetch('<%= solidstats.refresh_logs_path %>', {
273
+ method: 'POST',
274
+ headers: {
275
+ 'Content-Type': 'application/json',
276
+ 'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
277
+ }
278
+ })
279
+ .then(response => response.json())
280
+ .then(data => {
281
+ if (data.status === 'success') {
282
+ showToast('Log data refreshed successfully', 'success');
283
+ setTimeout(() => {
284
+ window.location.reload();
285
+ }, 2000);
286
+ } else {
287
+ showToast('Error refreshing log data', 'error');
288
+ }
289
+ })
290
+ .catch(error => {
291
+ showToast('Error refreshing log data', 'error');
292
+ });
293
+ }
294
+
295
+ function truncateLog(filename) {
296
+ if (confirm(`Are you sure you want to truncate ${filename}? This action cannot be undone.`)) {
297
+ showToast('Truncating log file...', 'info');
298
+
299
+ fetch(`<%= solidstats.truncate_log_path('FILENAME_PLACEHOLDER') %>`.replace('FILENAME_PLACEHOLDER', encodeURIComponent(filename)), {
300
+ method: 'POST',
301
+ headers: {
302
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
303
+ 'Content-Type': 'application/json'
304
+ }
305
+ })
306
+ .then(response => response.json())
307
+ .then(data => {
308
+ if (data.success) {
309
+ showToast(`${filename} truncated successfully`, 'success');
310
+ setTimeout(() => {
311
+ window.location.reload();
312
+ }, 2000);
313
+ } else {
314
+ showToast(data.error || 'Failed to truncate log file', 'error');
315
+ }
316
+ })
317
+ .catch(error => {
318
+ showToast('Error truncating log file', 'error');
319
+ });
320
+ }
321
+ }
322
+
323
+ function downloadLogsData() {
324
+ const logsData = <%= raw @logs_data.to_json %>;
325
+ const blob = new Blob([JSON.stringify(logsData, null, 2)], { type: 'application/json' });
326
+ const url = URL.createObjectURL(blob);
327
+ const a = document.createElement('a');
328
+ a.href = url;
329
+ a.download = `logs_data_${new Date().toISOString().split('T')[0]}.json`;
330
+ document.body.appendChild(a);
331
+ a.click();
332
+ document.body.removeChild(a);
333
+ URL.revokeObjectURL(url);
334
+ showToast('Logs data exported successfully', 'success');
335
+ }
336
+
337
+ function showToast(message, type = 'info') {
338
+ const toast = document.getElementById('toast');
339
+ const icon = document.getElementById('toast-icon');
340
+ const iconContainer = icon.parentElement;
341
+ const messageEl = document.getElementById('toast-message');
342
+
343
+ messageEl.textContent = message;
344
+
345
+ // Reset classes
346
+ icon.className = 'w-5 h-5';
347
+ iconContainer.className = 'flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center mr-3';
348
+
349
+ switch(type) {
350
+ case 'success':
351
+ icon.setAttribute('data-feather', 'check-circle');
352
+ icon.className += ' text-white';
353
+ iconContainer.className += ' bg-gradient-to-r from-emerald-500 to-green-500';
354
+ break;
355
+ case 'error':
356
+ icon.setAttribute('data-feather', 'alert-circle');
357
+ icon.className += ' text-white';
358
+ iconContainer.className += ' bg-gradient-to-r from-red-500 to-rose-500';
359
+ break;
360
+ case 'info':
361
+ default:
362
+ icon.setAttribute('data-feather', 'info');
363
+ icon.className += ' text-white';
364
+ iconContainer.className += ' bg-gradient-to-r from-blue-500 to-indigo-500';
365
+ break;
366
+ }
367
+
368
+ feather.replace();
369
+ toast.classList.remove('hidden');
370
+
371
+ setTimeout(() => {
372
+ hideToast();
373
+ }, 5000);
374
+ }
375
+
376
+ function hideToast() {
377
+ const toast = document.getElementById('toast');
378
+ toast.classList.add('hidden');
379
+ }
380
+
381
+ // Search and filter functionality
382
+ document.addEventListener('DOMContentLoaded', function() {
383
+ const searchInput = document.getElementById('logSearch');
384
+ const statusFilter = document.getElementById('logStatusFilter');
385
+ const tableRows = document.querySelectorAll('tbody tr');
386
+
387
+ function filterTable() {
388
+ const searchTerm = searchInput.value.toLowerCase();
389
+ const statusValue = statusFilter.value;
390
+
391
+ tableRows.forEach(row => {
392
+ const fileName = row.querySelector('td:first-child').textContent.toLowerCase();
393
+ const status = row.querySelector('td:nth-child(4)').textContent.toLowerCase().trim();
394
+
395
+ const matchesSearch = fileName.includes(searchTerm);
396
+ const matchesStatus = statusValue === 'all' || status === statusValue;
397
+
398
+ if (matchesSearch && matchesStatus) {
399
+ row.style.display = '';
400
+ } else {
401
+ row.style.display = 'none';
402
+ }
403
+ });
404
+ }
405
+
406
+ if (searchInput) searchInput.addEventListener('input', filterTable);
407
+ if (statusFilter) statusFilter.addEventListener('change', filterTable);
408
+ });
409
+ </script>
@@ -0,0 +1,158 @@
1
+ <% content_for :head do %>
2
+ <title>LoadLens - Performance Dashboard</title>
3
+ <% end %>
4
+
5
+ <div class="flex justify-between items-center mb-8">
6
+ <h1 class="text-3xl font-bold">LoadLens - Performance Dashboard</h1>
7
+ <%= link_to "Refresh Data", refresh_performance_index_path,
8
+ class: "btn btn-primary",
9
+ "data-method": :post %>
10
+ </div>
11
+
12
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
13
+ <!-- Total Requests -->
14
+ <%= render 'solidstats/shared/dashboard_card',
15
+ icon: 'activity',
16
+ status: 'info',
17
+ value: @metrics[:total_requests] || 0,
18
+ name: 'Total Requests',
19
+ last_updated: Time.current,
20
+ url: '#',
21
+ badges: [
22
+ { text: 'Last 7 Days', color: 'info' }
23
+ ] %>
24
+
25
+ <!-- Average Response Time -->
26
+ <%= render 'solidstats/shared/dashboard_card',
27
+ icon: 'clock',
28
+ status: (@metrics[:avg_response_time] || 0) > 1000 ? 'warning' : 'success',
29
+ value: "#{@metrics[:avg_response_time] || 0}ms",
30
+ name: 'Avg Response Time',
31
+ last_updated: Time.current,
32
+ url: '#',
33
+ badges: [
34
+ { text: (@metrics[:avg_response_time] || 0) > 1000 ? 'Slow' : 'Fast',
35
+ color: (@metrics[:avg_response_time] || 0) > 1000 ? 'warning' : 'success' }
36
+ ] %>
37
+
38
+ <!-- Database Time -->
39
+ <%= render 'solidstats/shared/dashboard_card',
40
+ icon: 'database',
41
+ status: (@metrics[:avg_db_time] || 0) > 500 ? 'warning' : 'success',
42
+ value: "#{@metrics[:avg_db_time] || 0}ms",
43
+ name: 'Avg DB Time',
44
+ last_updated: Time.current,
45
+ url: '#',
46
+ badges: [
47
+ { text: 'ActiveRecord', color: 'primary' }
48
+ ] %>
49
+
50
+ <!-- View Rendering Time -->
51
+ <%= render 'solidstats/shared/dashboard_card',
52
+ icon: 'eye',
53
+ status: (@metrics[:avg_view_time] || 0) > 300 ? 'warning' : 'success',
54
+ value: "#{@metrics[:avg_view_time] || 0}ms",
55
+ name: 'Avg View Time',
56
+ last_updated: Time.current,
57
+ url: '#',
58
+ badges: [
59
+ { text: 'Rendering', color: 'secondary' }
60
+ ] %>
61
+
62
+ <!-- Slow Requests -->
63
+ <%= render 'solidstats/shared/dashboard_card',
64
+ icon: 'alert-triangle',
65
+ status: (@metrics[:slow_requests] || 0) > 0 ? 'warning' : 'success',
66
+ value: @metrics[:slow_requests] || 0,
67
+ name: 'Slow Requests',
68
+ last_updated: Time.current,
69
+ url: '#',
70
+ badges: [
71
+ { text: '>1000ms', color: 'warning' }
72
+ ] %>
73
+
74
+ <!-- Error Rate -->
75
+ <%= render 'solidstats/shared/dashboard_card',
76
+ icon: 'alert-circle',
77
+ status: (@metrics[:error_rate] || 0) > 5 ? 'error' : 'success',
78
+ value: "#{@metrics[:error_rate] || 0}%",
79
+ name: 'Error Rate',
80
+ last_updated: Time.current,
81
+ url: '#',
82
+ badges: [
83
+ { text: '4xx/5xx', color: (@metrics[:error_rate] || 0) > 5 ? 'error' : 'success' }
84
+ ] %>
85
+ </div>
86
+
87
+ <!-- Recent Requests Table -->
88
+ <div class="card bg-base-100 shadow-lg">
89
+ <div class="card-body">
90
+ <h2 class="card-title">Recent Requests</h2>
91
+
92
+ <% if @recent_requests.any? %>
93
+ <div class="overflow-x-auto">
94
+ <table class="table table-zebra">
95
+ <thead>
96
+ <tr>
97
+ <th>Time</th>
98
+ <th>Controller#Action</th>
99
+ <th>Method</th>
100
+ <th>Status</th>
101
+ <th>Total (ms)</th>
102
+ <th>View (ms)</th>
103
+ <th>DB (ms)</th>
104
+ </tr>
105
+ </thead>
106
+ <tbody>
107
+ <% @recent_requests.each do |request| %>
108
+ <tr>
109
+ <td>
110
+ <div class="text-sm">
111
+ <%= Time.parse(request['timestamp']).strftime('%H:%M:%S') rescue 'N/A' %>
112
+ </div>
113
+ </td>
114
+ <td>
115
+ <div class="font-medium">
116
+ <%= request['controller'] %>#<%= request['action'] %>
117
+ </div>
118
+ <div class="text-sm opacity-50">
119
+ <%= request['path'] %>
120
+ </div>
121
+ </td>
122
+ <td>
123
+ <div class="badge badge-sm <%= request['http_method'] == 'GET' ? 'badge-success' : 'badge-primary' %>">
124
+ <%= request['http_method'] %>
125
+ </div>
126
+ </td>
127
+ <td>
128
+ <div class="badge badge-sm <%= (request['status'] || 200) < 400 ? 'badge-success' : 'badge-error' %>">
129
+ <%= request['status'] || 200 %>
130
+ </div>
131
+ </td>
132
+ <td>
133
+ <span class="<%= (request['total_time_ms'] || 0) > 1000 ? 'text-warning' : 'text-success' %>">
134
+ <%= ((request['total_time_ms'] || 0).round(1)) %>
135
+ </span>
136
+ </td>
137
+ <td><%= ((request['view_time_ms'] || 0).round(1)) %></td>
138
+ <td><%= ((request['activerecord_time_ms'] || 0).round(1)) %></td>
139
+ </tr>
140
+ <% end %>
141
+ </tbody>
142
+ </table>
143
+ </div>
144
+ <% else %>
145
+ <div class="text-center py-8">
146
+ <div class="text-base-content/60 mb-4">
147
+ <i data-feather="activity" class="w-12 h-12 mx-auto mb-2"></i>
148
+ <p class="text-lg font-medium">No LoadLens Data</p>
149
+ <p class="text-sm">Start using your Rails app in development to see performance metrics here.</p>
150
+ </div>
151
+ <div class="text-sm text-base-content/40">
152
+ <p>LoadLens data is collected automatically from your development.log file.</p>
153
+ </div>
154
+ </div>
155
+ <% end %>
156
+ </div>
157
+ </div>
158
+
@@ -0,0 +1,49 @@
1
+ <div class="space-y-4">
2
+ <h3 class="text-lg font-semibold mb-4"><%= title %> (<%= todos.length %>)</h3>
3
+
4
+ <% if todos.empty? %>
5
+ <div class="text-center py-8">
6
+ <div class="text-6xl mb-4">🎉</div>
7
+ <p class="text-base-content/70">No TODOs found!</p>
8
+ </div>
9
+ <% else %>
10
+ <div class="space-y-3">
11
+ <% todos.each do |todo| %>
12
+ <div class="card bg-base-200 shadow-sm">
13
+ <div class="card-body p-4">
14
+ <div class="flex items-start justify-between">
15
+ <div class="flex-1">
16
+ <div class="flex items-center gap-2 mb-2">
17
+ <span class="badge badge-<%= todo_type_color(todo[:type]) %> badge-sm">
18
+ <%= todo[:type].upcase %>
19
+ </span>
20
+ <span class="text-sm text-base-content/60">
21
+ <%= todo[:file] %>:<%= todo[:line_number] %>
22
+ </span>
23
+ </div>
24
+
25
+ <p class="text-sm font-mono bg-base-300 p-2 rounded">
26
+ <%= todo[:full_line] %>
27
+ </p>
28
+
29
+ <% if todo[:content] != todo[:full_line] %>
30
+ <p class="text-sm mt-2 text-base-content/80">
31
+ <%= todo[:content] %>
32
+ </p>
33
+ <% end %>
34
+ </div>
35
+
36
+ <div class="ml-4">
37
+ <button class="btn btn-ghost btn-xs" onclick="copyToClipboard('<%= todo[:file] %>:<%= todo[:line_number] %>')">
38
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
39
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
40
+ </svg>
41
+ </button>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ <% end %>
47
+ </div>
48
+ <% end %>
49
+ </div>