Class: Lyra::DashboardController

Inherits:
ApplicationController show all
Defined in:
app/controllers/lyra/dashboard_controller.rb

Instance Method Summary collapse

Methods inherited from ApplicationController

#pam_dsl_available?, #petri_flow_available?, #user_tracking_configured?

Instance Method Details

#audit_trail ⇒ Object

GET /lyra/dashboard/audit_trail



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'app/controllers/lyra/dashboard_controller.rb', line 114

def audit_trail
  @monitored_models = Lyra.config.monitored_models
  @selected_model = params[:model_class]
  @selected_id = params[:record_id]
  @search_query = params[:search]

  # If a model is selected, load available records for the picker
  if @selected_model.present?
    @model_class = @selected_model.constantize
    @available_records = fetch_available_records(@model_class, @search_query)
  end

  if @selected_model.present? && @selected_id.present?
    @record = @model_class.find_by(id: @selected_id)
    @audit_entries = AuditProjection.audit_trail(@model_class, @selected_id)
  end
end

#audit_trail_for_record ⇒ Object

GET /lyra/dashboard/audit_trail/:model_class/:id



133
134
135
136
137
138
139
140
141
142
143
144
# File 'app/controllers/lyra/dashboard_controller.rb', line 133

def audit_trail_for_record
  @model_class = params[:model_class].constantize
  @model_id = params[:id]
  @record = @model_class.find_by(id: @model_id)
  @audit_entries = AuditProjection.audit_trail(@model_class, @model_id)
  @monitored_models = Lyra.config.monitored_models

  respond_to do |format|
    format.html { render :audit_trail }
    format.json { render json: { audit_trail: @audit_entries, record_id: @model_id, model: @model_class.name } }
  end
end

#compare ⇒ Object

GET /lyra/dashboard/compare/:model_class/:id



20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'app/controllers/lyra/dashboard_controller.rb', line 20

def compare
  model_class = params[:model_class].constantize
  model_id = params[:id]

  @comparison = DualView.new(model_class, model_id).compare
  @audit_trail = DualView.new(model_class, model_id).audit_trail
  @analysis = StateAnalyzer.analyze(model_class, model_id)

  render json: {
    comparison: @comparison,
    audit_trail: @audit_trail,
    analysis: @analysis
  }
end

#discrepancies ⇒ Object

GET /lyra/dashboard/discrepancies/:model_class



36
37
38
39
40
41
42
43
44
# File 'app/controllers/lyra/dashboard_controller.rb', line 36

def discrepancies
  @model_class = params[:model_class].constantize
  @discrepancies = DualView.find_discrepancies(@model_class)

  respond_to do |format|
    format.html
    format.json { render json: { discrepancies: @discrepancies } }
  end
end

#entity_graph ⇒ Object

GET /lyra/dashboard/visualizations/entity_graph/:model_class/:id Returns focused graph for a specific entity's lifecycle



169
170
171
172
173
174
175
176
177
178
179
180
# File 'app/controllers/lyra/dashboard_controller.rb', line 169

def entity_graph
  model_class = params[:model_class]
  model_id = params[:id]
  depth = params[:depth]&.to_i || 1  # How many related entities to include

  events = fetch_entity_events(model_class, model_id, depth: depth)
  graph = Visualization::EventGraph.new(events)

  render json: graph.to_data.merge(
    focused_entity: { model_class: model_class, model_id: model_id }
  )
end

#event_graph ⇒ Object

GET /lyra/dashboard/visualizations/event_graph Returns filtered events based on query params



156
157
158
159
160
161
162
163
164
165
# File 'app/controllers/lyra/dashboard_controller.rb', line 156

def event_graph
  events = fetch_filtered_events(
    model_class: params[:model_class],
    operation: params[:operation],
    limit: params[:limit]&.to_i || 50
  )
  graph = Visualization::EventGraph.new(events)

  render json: graph.to_data
end

#event_graph_view ⇒ Object

GET /lyra/visualizations/event_graph (HTML view)



233
234
235
236
237
238
239
# File 'app/controllers/lyra/dashboard_controller.rb', line 233

def event_graph_view
  @limit = params[:limit]&.to_i || 50
  events = fetch_recent_events(limit: @limit)
  graph = Visualization::EventGraph.new(events)
  @graph_data = graph.to_data
  @mermaid = graph.to_mermaid
end

#event_list ⇒ Object

GET /lyra/dashboard/visualizations/event_list Returns list of entities with event counts for the picker UI



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/controllers/lyra/dashboard_controller.rb', line 184

def event_list
  model_filter = params[:model_class]
  limit = params[:limit]&.to_i || 20

  entities = []
  models = model_filter.present? ? [model_filter.constantize] : Lyra.config.monitored_models

  models.each do |model_class|
    model_class.order(updated_at: :desc).limit(limit).each do |record|
      stream_name = "#{model_class.name}$#{record.id}"
      begin
        event_count = Lyra.config.event_store.read.stream(stream_name).count
        next if event_count == 0

        # Get display name for the record
        display_name = record_display_name(record)

        entities << {
          model_class: model_class.name,
          model_id: record.id,
          display_name: display_name,
          event_count: event_count,
          updated_at: record.updated_at
        }
      rescue
        # Skip if stream doesn't exist
      end
    end
  end

  # Sort by event_count descending, then updated_at descending
  entities.sort_by! { |e| [-e[:event_count], -e[:updated_at].to_i] }
  entities = entities.first(limit)

  render json: {
    entities: entities,
    model_classes: Lyra.config.monitored_models.map(&:name)
  }
end

#heatmap ⇒ Object

GET /lyra/dashboard/visualizations/heatmap



225
226
227
228
229
230
# File 'app/controllers/lyra/dashboard_controller.rb', line 225

def heatmap
  events = fetch_events_for_period(days: params[:days]&.to_i || 7)
  heatmap = Visualization::ActivityHeatmap.new(events)

  render json: heatmap.to_data
end

#heatmap_view ⇒ Object

GET /lyra/visualizations/heatmap (HTML view)



242
243
244
245
246
247
248
249
250
# File 'app/controllers/lyra/dashboard_controller.rb', line 242

def heatmap_view
  @days = params[:days]&.to_i || 7
  events = fetch_events_for_period(days: @days)
  heatmap = Visualization::ActivityHeatmap.new(events)
  @heatmap_data = heatmap.to_data
  @hourly = heatmap.hourly_breakdown
  @daily = heatmap.daily_breakdown
  @operation_heatmap = heatmap.operation_heatmap
end

#index ⇒ Object

GET /lyra/dashboard



7
8
9
10
# File 'app/controllers/lyra/dashboard_controller.rb', line 7

def index
  @monitored_models = Lyra.config.monitored_models
  @mode = Lyra.config.mode
end

#model_heatmap ⇒ Object

GET /lyra/dashboard/visualizations/model_heatmap/:model_class



363
364
365
366
367
368
369
370
371
372
373
# File 'app/controllers/lyra/dashboard_controller.rb', line 363

def model_heatmap
  model_class = params[:model_class].constantize
  events = fetch_events_for_model(model_class, days: params[:days]&.to_i || 7)
  heatmap = Visualization::ActivityHeatmap.new(events)

  render json: {
    heatmap: heatmap.to_data,
    model_specific: heatmap.model_heatmap,
    operation_breakdown: heatmap.operation_heatmap
  }
end

#model_overview ⇒ Object

GET /lyra/dashboard/model/:model_class



13
14
15
16
17
# File 'app/controllers/lyra/dashboard_controller.rb', line 13

def model_overview
  @model_class = params[:model_class].constantize
  @records_count = @model_class.count
  @events_count = count_events_for_model(@model_class)
end

#projections ⇒ Object

GET /lyra/config/projections



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'app/controllers/lyra/dashboard_controller.rb', line 47

def projections
  @mode = Lyra.config.mode
  @projection_mode = Lyra.config.projection_mode
  @strict_projections = Lyra.config.strict_projections
  @monitored_models = Lyra.config.monitored_models
  @async_inline = Lyra.config.async_projections_inline

  # Projection types available in Lyra
  @projection_types = [
    {
      name: "StateProjection",
      description: "Rebuilds current state by replaying events. Used for dual-view comparison between DB and event-sourced state.",
      use_case: "Consistency checking, debugging, state recovery"
    },
    {
      name: "AuditProjection",
      description: "Generates chronological audit trail from events. Shows who changed what and when.",
      use_case: "Compliance reporting, change history, forensics"
    },
    {
      name: "ModelProjection",
      description: "Synchronizes ActiveRecord model tables from events using raw SQL (bypasses callbacks).",
      use_case: "Event sourcing mode - keeps read models in sync"
    },
    {
      name: "AsyncProjectionJob",
      description: "Background job for eventual consistency. Retries with exponential backoff on failures.",
      use_case: "High-throughput systems, async projection mode",
      queue: "lyra_projections",
      retry_attempts: 5
    }
  ]

  # Get model configurations
  @model_configs = {}
  @monitored_models.each do |model_class|
    config = Lyra.config.model_config(model_class)
    @model_configs[model_class.name] = {
      event_prefix: config.event_prefix,
      aggregate_class: config.aggregate_class,
      command_handler: config.command_handler,
      privacy_policy: config.privacy_policy
    }
  end

  # Get event counts per model with operation breakdown
  @model_stats = {}
  @monitored_models.each do |model_class|
    events = collect_events_for_model(model_class)
    record_count = model_class.count rescue 0

    # Group by operation
    by_operation = events.group_by { |e| e.data[:operation] || e.data["operation"] }

    @model_stats[model_class.name] = {
      events: events.count,
      records: record_count,
      avg_events_per_record: record_count > 0 ? (events.count.to_f / record_count).round(2) : 0,
      created: by_operation[:created]&.count || by_operation["created"]&.count || 0,
      updated: by_operation[:updated]&.count || by_operation["updated"]&.count || 0,
      destroyed: by_operation[:destroyed]&.count || by_operation["destroyed"]&.count || 0,
      last_event_at: events.max_by { |e| e.[:timestamp] || e.timestamp rescue Time.at(0) }&.then { |e| e.[:timestamp] || e.timestamp rescue nil }
    }
  end
end

#schema ⇒ Object

GET /lyra/dashboard/schema



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'app/controllers/lyra/dashboard_controller.rb', line 281

def schema
  @schema = Schema::Store.load_current || Schema::Generator.generate
  @report = Schema::Reporter.new.generate

  # Parse schema for view
  @version = @schema[:version]
  @fingerprint = @schema[:fingerprint]
  @created_at = @schema[:created_at]
  @lyra_version = @schema[:lyra_version]
  @configuration = @schema[:configuration] || {}
  @models = @schema[:models] || {}
  @summary = @schema[:summary] || {}

  # Load version history
  @history = Schema::Store.history.reverse  # Most recent first

  # Check for uncommitted changes
  if @version
    current_schema = Schema::Generator.generate
    @pending_changes = Schema::Diff.compare(@schema, current_schema)
    @has_pending_changes = @pending_changes.any?
  else
    @pending_changes = []
    @has_pending_changes = false
  end

  respond_to do |format|
    format.html
    format.json { render json: @schema }
  end
end

#schema_history ⇒ Object

GET /lyra/dashboard/schema/history



352
353
354
355
356
357
358
359
360
# File 'app/controllers/lyra/dashboard_controller.rb', line 352

def schema_history
  @history = Schema::Store.history.reverse  # Most recent first
  @current_version = Schema::Store.load_current&.dig(:version)

  respond_to do |format|
    format.html
    format.json { render json: { history: @history, current_version: @current_version } }
  end
end

#schema_version ⇒ Object

GET /lyra/dashboard/schema/:version



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'app/controllers/lyra/dashboard_controller.rb', line 314

def schema_version
  version = params[:version].to_i
  @schema = Schema::Store.load_version(version)

  unless @schema
    redirect_to schema_path, alert: "Schema version #{version} not found"
    return
  end

  # Schema metadata
  @version = @schema[:version]
  @fingerprint = @schema[:fingerprint]
  @created_at = @schema[:created_at]
  @lyra_version = @schema[:lyra_version]
  @rails_version = @schema[:rails_version]
  @configuration = @schema[:configuration] || {}
  @models = @schema[:models] || {}
  @summary = @schema[:summary] || {}

  # Load version history for navigation
  @history = Schema::Store.history
  @current_index = @history.find_index { |h| h[:version] == version }
  @prev_version = @current_index && @current_index > 0 ? @history[@current_index - 1][:version] : nil
  @next_version = @current_index && @current_index < @history.size - 1 ? @history[@current_index + 1][:version] : nil

  # Compare with previous version if available
  if @prev_version
    prev_schema = Schema::Store.load_version(@prev_version)
    @diff_from_previous = Schema::Diff.compare(prev_schema, @schema) if prev_schema
  end

  respond_to do |format|
    format.html
    format.json { render json: @schema }
  end
end

#timeline ⇒ Object

GET /lyra/dashboard/visualizations/timeline



147
148
149
150
151
152
# File 'app/controllers/lyra/dashboard_controller.rb', line 147

def timeline
  events = fetch_recent_events(limit: params[:limit]&.to_i || 100)
  timeline = Visualization::Timeline.new(events)

  render json: timeline.to_data
end

#verification ⇒ Object

GET /lyra/verification (HTML view)



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'app/controllers/lyra/dashboard_controller.rb', line 253

def verification
  @petri_flow_available = Lyra.petri_flow_available?

  if @petri_flow_available
    verifier = Lyra::Verification::CrudVerifier.new
    @report = verifier.verify_all
    @summary = @report[:summary]
    @lifecycle = @report[:details][:lifecycle]
    @modes = @report[:details][:modes]
    @generated_workflows = @report[:details][:generated_workflows] || {}
  else
    @report = nil
    @summary = nil
    @generated_workflows = {}
  end
end

#verification_data ⇒ Object

GET /lyra/verification.json



271
272
273
274
275
276
277
278
# File 'app/controllers/lyra/dashboard_controller.rb', line 271

def verification_data
  if Lyra.petri_flow_available?
    verifier = Lyra::Verification::CrudVerifier.new
    render json: verifier.verify_all
  else
    render json: { error: "PetriFlow not available", available: false }, status: :service_unavailable
  end
end