Class: Lyra::DashboardController
- Inherits:
-
ApplicationController
- Object
- ActionController::Base
- ApplicationController
- Lyra::DashboardController
- Defined in:
- app/controllers/lyra/dashboard_controller.rb
Instance Method Summary collapse
-
#audit_trail ⇒ Object
GET /lyra/dashboard/audit_trail.
-
#audit_trail_for_record ⇒ Object
GET /lyra/dashboard/audit_trail/:model_class/:id.
-
#compare ⇒ Object
GET /lyra/dashboard/compare/:model_class/:id.
-
#discrepancies ⇒ Object
GET /lyra/dashboard/discrepancies/:model_class.
-
#entity_graph ⇒ Object
GET /lyra/dashboard/visualizations/entity_graph/:model_class/:id Returns focused graph for a specific entity's lifecycle.
-
#event_graph ⇒ Object
GET /lyra/dashboard/visualizations/event_graph Returns filtered events based on query params.
-
#event_graph_view ⇒ Object
GET /lyra/visualizations/event_graph (HTML view).
-
#event_list ⇒ Object
GET /lyra/dashboard/visualizations/event_list Returns list of entities with event counts for the picker UI.
-
#heatmap ⇒ Object
GET /lyra/dashboard/visualizations/heatmap.
-
#heatmap_view ⇒ Object
GET /lyra/visualizations/heatmap (HTML view).
-
#index ⇒ Object
GET /lyra/dashboard.
-
#model_heatmap ⇒ Object
GET /lyra/dashboard/visualizations/model_heatmap/:model_class.
-
#model_overview ⇒ Object
GET /lyra/dashboard/model/:model_class.
-
#projections ⇒ Object
GET /lyra/config/projections.
-
#schema ⇒ Object
GET /lyra/dashboard/schema.
-
#schema_history ⇒ Object
GET /lyra/dashboard/schema/history.
-
#schema_version ⇒ Object
GET /lyra/dashboard/schema/:version.
-
#timeline ⇒ Object
GET /lyra/dashboard/visualizations/timeline.
-
#verification ⇒ Object
GET /lyra/verification (HTML view).
-
#verification_data ⇒ Object
GET /lyra/verification.json.
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. rescue Time.at(0) }&.then { |e| e.[:timestamp] || e. 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 |