solid_log-ui 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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +295 -0
  4. data/Rakefile +12 -0
  5. data/app/assets/javascripts/application.js +6 -0
  6. data/app/assets/javascripts/solid_log/checkbox_dropdown.js +171 -0
  7. data/app/assets/javascripts/solid_log/filter_state.js +138 -0
  8. data/app/assets/javascripts/solid_log/jump_to_live.js +119 -0
  9. data/app/assets/javascripts/solid_log/live_tail.js +476 -0
  10. data/app/assets/javascripts/solid_log/live_tail.js.bak +270 -0
  11. data/app/assets/javascripts/solid_log/log_filters.js +37 -0
  12. data/app/assets/javascripts/solid_log/stream_scroll.js +195 -0
  13. data/app/assets/javascripts/solid_log/timeline_histogram.js +162 -0
  14. data/app/assets/javascripts/solid_log/toast.js +50 -0
  15. data/app/assets/stylesheets/solid_log/application.css +1329 -0
  16. data/app/assets/stylesheets/solid_log/components.css +1506 -0
  17. data/app/assets/stylesheets/solid_log/mission_control.css +398 -0
  18. data/app/channels/solid_log/ui/application_cable/channel.rb +8 -0
  19. data/app/channels/solid_log/ui/application_cable/connection.rb +10 -0
  20. data/app/channels/solid_log/ui/log_stream_channel.rb +132 -0
  21. data/app/controllers/solid_log/ui/base_controller.rb +122 -0
  22. data/app/controllers/solid_log/ui/dashboard_controller.rb +32 -0
  23. data/app/controllers/solid_log/ui/entries_controller.rb +34 -0
  24. data/app/controllers/solid_log/ui/fields_controller.rb +57 -0
  25. data/app/controllers/solid_log/ui/streams_controller.rb +204 -0
  26. data/app/controllers/solid_log/ui/timelines_controller.rb +29 -0
  27. data/app/controllers/solid_log/ui/tokens_controller.rb +46 -0
  28. data/app/helpers/solid_log/ui/application_helper.rb +99 -0
  29. data/app/helpers/solid_log/ui/dashboard_helper.rb +46 -0
  30. data/app/helpers/solid_log/ui/entries_helper.rb +16 -0
  31. data/app/helpers/solid_log/ui/timeline_helper.rb +39 -0
  32. data/app/services/solid_log/ui/live_tail_broadcaster.rb +81 -0
  33. data/app/views/layouts/solid_log/ui/application.html.erb +53 -0
  34. data/app/views/solid_log/ui/dashboard/index.html.erb +178 -0
  35. data/app/views/solid_log/ui/entries/show.html.erb +132 -0
  36. data/app/views/solid_log/ui/fields/index.html.erb +133 -0
  37. data/app/views/solid_log/ui/shared/_checkbox_dropdown.html.erb +64 -0
  38. data/app/views/solid_log/ui/shared/_multiselect_filter.html.erb +37 -0
  39. data/app/views/solid_log/ui/shared/_toast.html.erb +7 -0
  40. data/app/views/solid_log/ui/shared/_toast_message.html.erb +30 -0
  41. data/app/views/solid_log/ui/streams/_filter_form.html.erb +207 -0
  42. data/app/views/solid_log/ui/streams/_footer.html.erb +37 -0
  43. data/app/views/solid_log/ui/streams/_log_entries.html.erb +5 -0
  44. data/app/views/solid_log/ui/streams/_log_row.html.erb +5 -0
  45. data/app/views/solid_log/ui/streams/_log_row_compact.html.erb +37 -0
  46. data/app/views/solid_log/ui/streams/_log_row_expanded.html.erb +67 -0
  47. data/app/views/solid_log/ui/streams/_log_stream_content.html.erb +8 -0
  48. data/app/views/solid_log/ui/streams/_timeline.html.erb +68 -0
  49. data/app/views/solid_log/ui/streams/index.html.erb +22 -0
  50. data/app/views/solid_log/ui/timelines/show_job.html.erb +78 -0
  51. data/app/views/solid_log/ui/timelines/show_request.html.erb +88 -0
  52. data/app/views/solid_log/ui/tokens/index.html.erb +95 -0
  53. data/app/views/solid_log/ui/tokens/new.html.erb +47 -0
  54. data/config/importmap.rb +15 -0
  55. data/config/routes.rb +27 -0
  56. data/lib/solid_log/ui/api_client.rb +117 -0
  57. data/lib/solid_log/ui/configuration.rb +99 -0
  58. data/lib/solid_log/ui/data_source.rb +146 -0
  59. data/lib/solid_log/ui/engine.rb +76 -0
  60. data/lib/solid_log/ui/version.rb +5 -0
  61. data/lib/solid_log/ui.rb +27 -0
  62. data/lib/solid_log-ui.rb +2 -0
  63. metadata +290 -0
@@ -0,0 +1,398 @@
1
+ /* SolidLog - Mission Control Inspired Styles */
2
+
3
+ :root {
4
+ --color-primary: #2563eb;
5
+ --color-success: #10b981;
6
+ --color-warning: #f59e0b;
7
+ --color-danger: #ef4444;
8
+ --color-info: #3b82f6;
9
+
10
+ --color-gray-50: #f9fafb;
11
+ --color-gray-100: #f3f4f6;
12
+ --color-gray-200: #e5e7eb;
13
+ --color-gray-300: #d1d5db;
14
+ --color-gray-700: #374151;
15
+ --color-gray-800: #1f2937;
16
+ --color-gray-900: #111827;
17
+
18
+ --spacing-sm: 0.5rem;
19
+ --spacing-md: 1rem;
20
+ --spacing-lg: 1.5rem;
21
+ --spacing-xl: 2rem;
22
+
23
+ --border-radius: 0.375rem;
24
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
25
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
26
+ }
27
+
28
+ body {
29
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
30
+ font-size: 14px;
31
+ line-height: 1.5;
32
+ color: var(--color-gray-900);
33
+ background: var(--color-gray-50);
34
+ margin: 0;
35
+ padding: 0;
36
+ display: flex;
37
+ flex-direction: column;
38
+ min-height: 100vh;
39
+ }
40
+
41
+ /* Navigation */
42
+ .top-nav {
43
+ background: white;
44
+ border-bottom: 1px solid var(--color-gray-200);
45
+ padding: 0 var(--spacing-xl);
46
+ display: flex;
47
+ align-items: center;
48
+ gap: var(--spacing-xl);
49
+ height: 60px;
50
+ }
51
+
52
+ .nav-brand {
53
+ font-size: 1.25rem;
54
+ }
55
+
56
+ .brand-link {
57
+ color: var(--color-gray-900);
58
+ text-decoration: none;
59
+ }
60
+
61
+ .brand-link:hover {
62
+ color: var(--color-primary);
63
+ }
64
+
65
+ .nav-links {
66
+ display: flex;
67
+ gap: var(--spacing-md);
68
+ }
69
+
70
+ .nav-link {
71
+ padding: 0.5rem 1rem;
72
+ color: var(--color-gray-700);
73
+ text-decoration: none;
74
+ font-weight: 500;
75
+ border-radius: var(--border-radius);
76
+ transition: all 0.15s ease;
77
+ }
78
+
79
+ .nav-link:hover {
80
+ background: var(--color-gray-100);
81
+ color: var(--color-gray-900);
82
+ }
83
+
84
+ .nav-link.active {
85
+ background: var(--color-primary);
86
+ color: white;
87
+ }
88
+
89
+ /* Main Content */
90
+ .main-content {
91
+ flex: 1;
92
+ }
93
+
94
+ /* Footer */
95
+ .footer {
96
+ background: white;
97
+ border-top: 1px solid var(--color-gray-200);
98
+ padding: var(--spacing-md) var(--spacing-xl);
99
+ text-align: center;
100
+ }
101
+
102
+ .footer-content {
103
+ font-size: 0.75rem;
104
+ color: var(--color-gray-700);
105
+ }
106
+
107
+ /* Page Header */
108
+ .page-header {
109
+ padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-lg);
110
+ background: white;
111
+ border-bottom: 1px solid var(--color-gray-200);
112
+ display: flex;
113
+ justify-content: space-between;
114
+ align-items: center;
115
+ }
116
+
117
+ .page-header h1 {
118
+ margin: 0;
119
+ font-size: 1.875rem;
120
+ font-weight: 700;
121
+ color: var(--color-gray-900);
122
+ }
123
+
124
+ .page-header .subtitle {
125
+ margin: 0.25rem 0 0;
126
+ color: var(--color-gray-700);
127
+ font-size: 0.875rem;
128
+ }
129
+
130
+ .page-actions {
131
+ display: flex;
132
+ gap: var(--spacing-sm);
133
+ }
134
+
135
+ /* Dashboard */
136
+ .dashboard {
137
+ max-width: 1400px;
138
+ margin: 0 auto;
139
+ padding: var(--spacing-xl);
140
+ }
141
+
142
+ .stats-grid {
143
+ display: grid;
144
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
145
+ gap: var(--spacing-lg);
146
+ margin-bottom: var(--spacing-xl);
147
+ }
148
+
149
+ .stat-card {
150
+ background: white;
151
+ border-radius: var(--border-radius);
152
+ padding: var(--spacing-lg);
153
+ box-shadow: var(--shadow-sm);
154
+ border: 1px solid var(--color-gray-200);
155
+ }
156
+
157
+ .stat-label {
158
+ font-size: 0.875rem;
159
+ color: var(--color-gray-700);
160
+ font-weight: 500;
161
+ margin-bottom: var(--spacing-sm);
162
+ }
163
+
164
+ .stat-value {
165
+ font-size: 2rem;
166
+ font-weight: 700;
167
+ color: var(--color-gray-900);
168
+ margin-bottom: var(--spacing-sm);
169
+ }
170
+
171
+ .stat-value.error-count {
172
+ color: var(--color-danger);
173
+ }
174
+
175
+ .stat-footer {
176
+ font-size: 0.75rem;
177
+ color: var(--color-gray-700);
178
+ }
179
+
180
+ .stat-link {
181
+ color: var(--color-primary);
182
+ text-decoration: none;
183
+ }
184
+
185
+ .stat-link:hover {
186
+ text-decoration: underline;
187
+ }
188
+
189
+ /* Dashboard Row */
190
+ .dashboard-row {
191
+ display: grid;
192
+ grid-template-columns: repeat(12, 1fr);
193
+ gap: var(--spacing-lg);
194
+ }
195
+
196
+ .dashboard-col-8 {
197
+ grid-column: span 8;
198
+ }
199
+
200
+ .dashboard-col-4 {
201
+ grid-column: span 4;
202
+ }
203
+
204
+ @media (max-width: 768px) {
205
+ .dashboard-col-8,
206
+ .dashboard-col-4 {
207
+ grid-column: span 12;
208
+ }
209
+ }
210
+
211
+ /* Card */
212
+ .card {
213
+ background: white;
214
+ border-radius: var(--border-radius);
215
+ box-shadow: var(--shadow-sm);
216
+ border: 1px solid var(--color-gray-200);
217
+ overflow: hidden;
218
+ }
219
+
220
+ .card-header {
221
+ padding: var(--spacing-md) var(--spacing-lg);
222
+ border-bottom: 1px solid var(--color-gray-200);
223
+ background: var(--color-gray-50);
224
+ }
225
+
226
+ .card-header h2 {
227
+ margin: 0;
228
+ font-size: 1.125rem;
229
+ font-weight: 600;
230
+ color: var(--color-gray-900);
231
+ }
232
+
233
+ .card-body {
234
+ padding: var(--spacing-lg);
235
+ }
236
+
237
+ .empty-state {
238
+ text-align: center;
239
+ padding: var(--spacing-xl);
240
+ color: var(--color-gray-700);
241
+ }
242
+
243
+ /* Log Items */
244
+ .log-list {
245
+ display: flex;
246
+ flex-direction: column;
247
+ gap: var(--spacing-md);
248
+ }
249
+
250
+ .log-item {
251
+ padding: var(--spacing-md);
252
+ border: 1px solid var(--color-gray-200);
253
+ border-radius: var(--border-radius);
254
+ background: var(--color-gray-50);
255
+ }
256
+
257
+ .log-item-header {
258
+ display: flex;
259
+ align-items: center;
260
+ gap: var(--spacing-sm);
261
+ margin-bottom: var(--spacing-sm);
262
+ }
263
+
264
+ .log-time {
265
+ font-size: 0.75rem;
266
+ color: var(--color-gray-700);
267
+ }
268
+
269
+ .log-message {
270
+ font-size: 0.875rem;
271
+ color: var(--color-gray-900);
272
+ margin-bottom: var(--spacing-sm);
273
+ }
274
+
275
+ .log-message a {
276
+ color: var(--color-gray-900);
277
+ text-decoration: none;
278
+ }
279
+
280
+ .log-message a:hover {
281
+ color: var(--color-primary);
282
+ }
283
+
284
+ .log-meta {
285
+ font-size: 0.75rem;
286
+ color: var(--color-gray-700);
287
+ }
288
+
289
+ /* Distribution */
290
+ .distribution-list {
291
+ display: flex;
292
+ flex-direction: column;
293
+ gap: var(--spacing-md);
294
+ }
295
+
296
+ .distribution-item {
297
+ display: flex;
298
+ flex-direction: column;
299
+ gap: 0.25rem;
300
+ }
301
+
302
+ .distribution-header {
303
+ display: flex;
304
+ justify-content: space-between;
305
+ align-items: center;
306
+ }
307
+
308
+ .distribution-count {
309
+ font-weight: 600;
310
+ color: var(--color-gray-900);
311
+ }
312
+
313
+ .distribution-bar-container {
314
+ height: 8px;
315
+ background: var(--color-gray-200);
316
+ border-radius: 4px;
317
+ overflow: hidden;
318
+ }
319
+
320
+ .distribution-bar {
321
+ height: 100%;
322
+ background: var(--color-primary);
323
+ transition: width 0.3s ease;
324
+ }
325
+
326
+ .distribution-percentage {
327
+ font-size: 0.75rem;
328
+ color: var(--color-gray-700);
329
+ text-align: right;
330
+ }
331
+
332
+ /* Quick Stats */
333
+ .quick-stats {
334
+ display: flex;
335
+ flex-direction: column;
336
+ gap: var(--spacing-md);
337
+ }
338
+
339
+ .quick-stat {
340
+ display: flex;
341
+ justify-content: space-between;
342
+ padding: var(--spacing-sm) 0;
343
+ border-bottom: 1px solid var(--color-gray-200);
344
+ }
345
+
346
+ .quick-stat:last-child {
347
+ border-bottom: none;
348
+ }
349
+
350
+ .quick-stat-label {
351
+ font-size: 0.875rem;
352
+ color: var(--color-gray-700);
353
+ }
354
+
355
+ .quick-stat-value {
356
+ font-size: 0.875rem;
357
+ font-weight: 600;
358
+ color: var(--color-gray-900);
359
+ }
360
+
361
+ /* Recommendations */
362
+ .recommendation-list {
363
+ display: flex;
364
+ flex-direction: column;
365
+ gap: var(--spacing-md);
366
+ }
367
+
368
+ .recommendation-item {
369
+ padding: var(--spacing-md);
370
+ background: var(--color-gray-50);
371
+ border: 1px solid var(--color-gray-200);
372
+ border-radius: var(--border-radius);
373
+ }
374
+
375
+ .recommendation-header {
376
+ display: flex;
377
+ justify-content: space-between;
378
+ align-items: center;
379
+ margin-bottom: var(--spacing-sm);
380
+ }
381
+
382
+ .recommendation-header code {
383
+ font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace;
384
+ font-size: 0.875rem;
385
+ font-weight: 600;
386
+ }
387
+
388
+ .recommendation-meta {
389
+ font-size: 0.8125rem;
390
+ color: var(--color-gray-600);
391
+ margin-bottom: var(--spacing-sm);
392
+ }
393
+
394
+ .recommendation-action {
395
+ font-size: 0.875rem;
396
+ color: var(--color-gray-700);
397
+ line-height: 1.5;
398
+ }
@@ -0,0 +1,8 @@
1
+ module SolidLog
2
+ module UI
3
+ module ApplicationCable
4
+ class Channel < ActionCable::Channel::Base
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ module SolidLog
2
+ module UI
3
+ module ApplicationCable
4
+ class Connection < ActionCable::Connection::Base
5
+ # No authentication needed for log streaming
6
+ # In production, you might want to add token-based auth
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,132 @@
1
+ module SolidLog
2
+ module UI
3
+ class LogStreamChannel < ApplicationCable::Channel
4
+ CACHE_NAMESPACE = "solid_log:active_filters"
5
+ CACHE_EXPIRY = 5.minutes
6
+
7
+ def subscribed
8
+ # Store the filters for this subscription
9
+ @filters = params[:filters] || {}
10
+ @filter_key = generate_filter_key(@filters)
11
+
12
+ # Subscribe to new entries broadcast from service
13
+ # Service broadcasts entry IDs, we filter and render them
14
+ stream_from "solid_log_new_entries", coder: ActiveSupport::JSON do |data|
15
+ handle_new_entries(data["entry_ids"]) if data["entry_ids"]
16
+ end
17
+
18
+ # Register this filter combination in Rails.cache
19
+ # Expires after 5 minutes of inactivity (refreshed on heartbeat)
20
+ cache_key = "#{CACHE_NAMESPACE}:#{@filter_key}"
21
+ Rails.cache.write(cache_key, @filters, expires_in: CACHE_EXPIRY)
22
+
23
+ # Also add to the set of active filter keys
24
+ register_active_filter_key(@filter_key)
25
+ end
26
+
27
+ def unsubscribed
28
+ # Cleanup when channel is unsubscribed
29
+ stop_all_streams
30
+
31
+ # Cache entries will expire naturally after CACHE_EXPIRY
32
+ # This handles the case where multiple clients use same filters
33
+ end
34
+
35
+ def refresh_subscription
36
+ # Called periodically by client to keep subscription active in cache
37
+ # This prevents the filter from expiring while user is actively watching
38
+ if @filter_key
39
+ cache_key = "#{CACHE_NAMESPACE}:#{@filter_key}"
40
+ Rails.cache.write(cache_key, @filters, expires_in: CACHE_EXPIRY)
41
+ register_active_filter_key(@filter_key)
42
+ end
43
+ end
44
+
45
+ def self.active_filter_combinations
46
+ # Read all active filter combinations from cache
47
+ active_keys = Rails.cache.read("#{CACHE_NAMESPACE}:keys") || []
48
+
49
+ filters_hash = {}
50
+ active_keys.each do |key|
51
+ cache_key = "#{CACHE_NAMESPACE}:#{key}"
52
+ filters = Rails.cache.read(cache_key)
53
+ filters_hash[key] = filters if filters
54
+ end
55
+
56
+ filters_hash
57
+ end
58
+
59
+ private
60
+
61
+ def generate_filter_key(filters)
62
+ # Create a consistent hash based on filter values
63
+ # Sort to ensure same filters = same key regardless of order
64
+ normalized = filters.sort.to_h
65
+ Digest::MD5.hexdigest(normalized.to_json)
66
+ end
67
+
68
+ def register_active_filter_key(filter_key)
69
+ # Maintain a list of active filter keys in cache
70
+ keys_cache_key = "#{CACHE_NAMESPACE}:keys"
71
+
72
+ # Read current keys, add this one, write back
73
+ # Note: This has a race condition but it's acceptable for this use case
74
+ current_keys = Rails.cache.read(keys_cache_key) || []
75
+ current_keys << filter_key unless current_keys.include?(filter_key)
76
+
77
+ # Keep the list alive as long as any filter is active
78
+ Rails.cache.write(keys_cache_key, current_keys.uniq, expires_in: CACHE_EXPIRY)
79
+ end
80
+
81
+ def handle_new_entries(entry_ids)
82
+ return if entry_ids.blank?
83
+
84
+ Rails.logger.info "[LogStreamChannel] Received broadcast with #{entry_ids.size} entry IDs: #{entry_ids.first(5)}"
85
+
86
+ # Fetch entries matching these IDs
87
+ entries = SolidLog::Entry.where(id: entry_ids).order(:id)
88
+ Rails.logger.info "[LogStreamChannel] Found #{entries.size} entries in database"
89
+
90
+ # Filter to only entries matching this client's filters
91
+ transmitted_count = 0
92
+ entries.each do |entry|
93
+ matches = entry_matches_filters?(entry)
94
+ Rails.logger.debug "[LogStreamChannel] Entry #{entry.id} matches filters: #{matches}"
95
+ next unless matches
96
+
97
+ # Render HTML for this specific entry with proper route context
98
+ html = SolidLog::UI::BaseController.render(
99
+ partial: "solid_log/ui/streams/log_row",
100
+ locals: { entry: entry, query: nil },
101
+ layout: false
102
+ )
103
+
104
+ # Transmit to this specific client
105
+ transmit({ html: html, entry_id: entry.id })
106
+ transmitted_count += 1
107
+ end
108
+
109
+ Rails.logger.info "[LogStreamChannel] Transmitted #{transmitted_count} entries to client (filter: #{@filter_key})"
110
+ end
111
+
112
+ def entry_matches_filters?(entry)
113
+ return true if @filters.blank?
114
+
115
+ # Check each filter condition
116
+ @filters.each do |key, values|
117
+ values = Array(values).reject(&:blank?)
118
+ next if values.empty?
119
+
120
+ entry_value = entry.public_send(key) rescue nil
121
+ return false if entry_value.nil?
122
+
123
+ unless values.map(&:to_s).include?(entry_value.to_s)
124
+ return false
125
+ end
126
+ end
127
+
128
+ true
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,122 @@
1
+ module SolidLog
2
+ module UI
3
+ # Dynamically inherit from configured base controller
4
+ base_controller_class = begin
5
+ SolidLog::UI.configuration.base_controller.constantize
6
+ rescue NameError
7
+ # Fallback to ActionController::Base if configuration not set or class doesn't exist
8
+ ActionController::Base
9
+ end
10
+
11
+ class BaseController < base_controller_class
12
+ include Turbo::Streams::TurboStreamsTagBuilder
13
+ helper Turbo::Engine.helpers
14
+ helper Importmap::ImportmapTagsHelper
15
+
16
+ # Explicitly include engine helpers (now that they're in correct namespace path)
17
+ helper SolidLog::UI::ApplicationHelper
18
+ helper SolidLog::UI::DashboardHelper
19
+ helper SolidLog::UI::EntriesHelper
20
+ helper SolidLog::UI::TimelineHelper
21
+
22
+ layout "solid_log/ui/application"
23
+
24
+ before_action :authenticate_user!
25
+ before_action :set_data_source
26
+
27
+ # Override this method in your host application to implement custom authentication
28
+ #
29
+ # Example configurations in config/initializers/solid_log_ui.rb:
30
+ #
31
+ # 1. Using a Proc/Lambda:
32
+ # SolidLog::UI.configure do |config|
33
+ # config.authentication_method = ->(controller) {
34
+ # controller.redirect_to controller.root_path unless controller.current_user&.admin?
35
+ # }
36
+ # end
37
+ #
38
+ # 2. Using a method name:
39
+ # SolidLog::UI.configure do |config|
40
+ # config.authentication_method = :require_admin
41
+ # end
42
+ #
43
+ # class ApplicationController
44
+ # def require_admin
45
+ # redirect_to root_path unless current_user&.admin?
46
+ # end
47
+ # end
48
+ #
49
+ # 3. Using basic auth:
50
+ # SolidLog::UI.configure do |config|
51
+ # config.authentication_method = :basic
52
+ # end
53
+ #
54
+ def authenticate_user!
55
+ config = SolidLog::UI.configuration
56
+ auth_method = config.authentication_method
57
+
58
+ case auth_method
59
+ when :none
60
+ # No authentication required
61
+ true
62
+ when :basic
63
+ authenticate_or_request_with_http_basic("SolidLog") do |username, password|
64
+ authenticate_with_basic_auth(username, password)
65
+ end
66
+ when Proc
67
+ # Call the proc in the controller's context
68
+ instance_exec(&auth_method)
69
+ when Symbol
70
+ # Call the named method on the controller
71
+ if respond_to?(auth_method, true)
72
+ send(auth_method)
73
+ else
74
+ raise NoMethodError, "Authentication method '#{auth_method}' not defined. Define it in your ApplicationController or BaseController."
75
+ end
76
+ else
77
+ render plain: "Invalid authentication configuration", status: :unauthorized
78
+ false
79
+ end
80
+ end
81
+
82
+ protected
83
+
84
+ # Override this in host app to customize basic auth credentials
85
+ def authenticate_with_basic_auth(username, password)
86
+ # Default: check Rails credentials
87
+ credentials = Rails.application.credentials.solidlog || {}
88
+ username == credentials[:username] && password == credentials[:password]
89
+ end
90
+
91
+ # Helper to check if user is authenticated
92
+ def authenticated?
93
+ auth_method = SolidLog::UI.configuration.authentication_method
94
+
95
+ case auth_method
96
+ when :none
97
+ true
98
+ when :basic
99
+ request.authorization.present?
100
+ when Proc, Symbol
101
+ # For custom auth (proc or method name), assume authenticated if we got this far
102
+ # (since authenticate_user! would have redirected/rendered if not authenticated)
103
+ true
104
+ else
105
+ false
106
+ end
107
+ end
108
+
109
+ # Current user - override in host app if using custom auth
110
+ def current_user
111
+ nil
112
+ end
113
+ helper_method :current_user
114
+
115
+ private
116
+
117
+ def set_data_source
118
+ @data_source = SolidLog::UI::DataSource.new
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,32 @@
1
+ module SolidLog
2
+ module UI
3
+ class DashboardController < BaseController
4
+ def index
5
+ @health_metrics = SolidLog.without_logging { SolidLog::HealthService.metrics }
6
+ @recent_errors = recent_error_entries
7
+ @log_level_distribution = log_level_distribution
8
+ @field_recommendations = field_recommendations
9
+ end
10
+
11
+ private
12
+
13
+ def recent_error_entries
14
+ SolidLog.without_logging do
15
+ Entry.errors.recent.limit(10)
16
+ end
17
+ end
18
+
19
+ def log_level_distribution
20
+ SolidLog.without_logging do
21
+ Entry.group(:level).count
22
+ end
23
+ end
24
+
25
+ def field_recommendations
26
+ SolidLog.without_logging do
27
+ SolidLog::FieldAnalyzer.analyze.take(5)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end