rails_nexus 2.0.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 (114) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +430 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/config/rails_nexus_manifest.js +2 -0
  6. data/app/assets/javascripts/rails_nexus/controllers.js +603 -0
  7. data/app/assets/stylesheets/rails_nexus/application.css +1964 -0
  8. data/app/controllers/rails_nexus/analytics_controller.rb +361 -0
  9. data/app/controllers/rails_nexus/application_controller.rb +15 -0
  10. data/app/controllers/rails_nexus/backup_controller.rb +91 -0
  11. data/app/controllers/rails_nexus/cron_jobs_controller.rb +64 -0
  12. data/app/controllers/rails_nexus/database_health_controller.rb +128 -0
  13. data/app/controllers/rails_nexus/logged_exceptions_controller.rb +227 -0
  14. data/app/controllers/rails_nexus/n1_patterns_controller.rb +10 -0
  15. data/app/controllers/rails_nexus/settings_controller.rb +100 -0
  16. data/app/controllers/rails_nexus/source_code_controller.rb +39 -0
  17. data/app/controllers/rails_nexus/stats_controller.rb +494 -0
  18. data/app/controllers/rails_nexus/workflow_controller.rb +96 -0
  19. data/app/helpers/rails_nexus/application_helper.rb +5 -0
  20. data/app/helpers/rails_nexus/logged_exceptions_helper.rb +254 -0
  21. data/app/helpers/rails_nexus/source_code_helper.rb +107 -0
  22. data/app/javascript/controllers/rails_nexus_controller.js +380 -0
  23. data/app/javascript/controllers/rails_nexus_detail_controller.js +165 -0
  24. data/app/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  25. data/app/javascript/controllers/rails_nexus_theme_controller.js +31 -0
  26. data/app/javascript/rails_nexus/application.js +17 -0
  27. data/app/jobs/rails_nexus/application_job.rb +4 -0
  28. data/app/mailers/rails_nexus/application_mailer.rb +6 -0
  29. data/app/models/rails_nexus/application_record.rb +5 -0
  30. data/app/models/rails_nexus/backup.rb +107 -0
  31. data/app/models/rails_nexus/base_record.rb +26 -0
  32. data/app/models/rails_nexus/comment.rb +13 -0
  33. data/app/models/rails_nexus/cron_job.rb +72 -0
  34. data/app/models/rails_nexus/database_stat.rb +128 -0
  35. data/app/models/rails_nexus/event.rb +101 -0
  36. data/app/models/rails_nexus/logged_exception.rb +864 -0
  37. data/app/models/rails_nexus/logged_exception_n1.rb +87 -0
  38. data/app/models/rails_nexus/metric.rb +107 -0
  39. data/app/models/rails_nexus/nginx_metric.rb +112 -0
  40. data/app/models/rails_nexus/server_metric.rb +168 -0
  41. data/app/models/rails_nexus/webhook_delivery.rb +64 -0
  42. data/app/services/rails_nexus/backup_service.rb +275 -0
  43. data/app/views/layouts/rails_nexus/application.html.erb +185 -0
  44. data/app/views/rails_nexus/analytics/index.html.erb +363 -0
  45. data/app/views/rails_nexus/backup/files.html.erb +53 -0
  46. data/app/views/rails_nexus/backup/index.html.erb +211 -0
  47. data/app/views/rails_nexus/backup/settings.html.erb +158 -0
  48. data/app/views/rails_nexus/cron_jobs/index.html.erb +141 -0
  49. data/app/views/rails_nexus/database_health/index.html.erb +139 -0
  50. data/app/views/rails_nexus/logged_exceptions/_comments_list.html.erb +50 -0
  51. data/app/views/rails_nexus/logged_exceptions/_exceptions.html.erb +190 -0
  52. data/app/views/rails_nexus/logged_exceptions/_feed.html.erb +3 -0
  53. data/app/views/rails_nexus/logged_exceptions/_filter_group.html.erb +10 -0
  54. data/app/views/rails_nexus/logged_exceptions/_show.html.erb +554 -0
  55. data/app/views/rails_nexus/logged_exceptions/_workflow_panel.html.erb +118 -0
  56. data/app/views/rails_nexus/logged_exceptions/clear.turbo_stream.erb +6 -0
  57. data/app/views/rails_nexus/logged_exceptions/destroy.turbo_stream.erb +2 -0
  58. data/app/views/rails_nexus/logged_exceptions/destroy_all.turbo_stream.erb +6 -0
  59. data/app/views/rails_nexus/logged_exceptions/feed.rss.builder +20 -0
  60. data/app/views/rails_nexus/logged_exceptions/index.html.erb +323 -0
  61. data/app/views/rails_nexus/logged_exceptions/query.html.erb +3 -0
  62. data/app/views/rails_nexus/logged_exceptions/query.turbo_stream.erb +6 -0
  63. data/app/views/rails_nexus/logged_exceptions/show.html.erb +3 -0
  64. data/app/views/rails_nexus/logged_exceptions/show.turbo_stream.erb +5 -0
  65. data/app/views/rails_nexus/n1_patterns/index.html.erb +153 -0
  66. data/app/views/rails_nexus/settings/index.html.erb +140 -0
  67. data/app/views/rails_nexus/stats/index.html.erb +569 -0
  68. data/config/importmap.rb +15 -0
  69. data/config/initializers/date_formats.rb +5 -0
  70. data/config/initializers/rails_nexus.rb +18 -0
  71. data/config/locales/en.yml +47 -0
  72. data/config/routes.rb +58 -0
  73. data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +22 -0
  74. data/lib/generators/rails_nexus/backup_generator.rb +70 -0
  75. data/lib/generators/rails_nexus/customize_generator.rb +159 -0
  76. data/lib/generators/rails_nexus/install_generator.rb +61 -0
  77. data/lib/generators/rails_nexus/templates/INSTALL.md +9 -0
  78. data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +152 -0
  79. data/lib/generators/rails_nexus/templates/backup/config.rb +27 -0
  80. data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +24 -0
  81. data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +35 -0
  82. data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +43 -0
  83. data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +15 -0
  84. data/lib/generators/rails_nexus/templates/backup/schedule.rb +28 -0
  85. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_controller.js +310 -0
  86. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_detail_controller.js +98 -0
  87. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  88. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_theme_controller.js +30 -0
  89. data/lib/generators/rails_nexus/templates/migration.rb +24 -0
  90. data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +30 -0
  91. data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +22 -0
  92. data/lib/generators/rails_nexus/templates/migration_new_schema.rb +120 -0
  93. data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +11 -0
  94. data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +21 -0
  95. data/lib/generators/rails_nexus/templates/migration_workflow.rb +29 -0
  96. data/lib/generators/rails_nexus/templates/rails_nexus.rb +96 -0
  97. data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +129 -0
  98. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_exceptions.html.erb +123 -0
  99. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_show.html.erb +297 -0
  100. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/index.html.erb +113 -0
  101. data/lib/rails_nexus/breadcrumbs.rb +161 -0
  102. data/lib/rails_nexus/cleanup.rb +32 -0
  103. data/lib/rails_nexus/configuration.rb +319 -0
  104. data/lib/rails_nexus/engine.rb +135 -0
  105. data/lib/rails_nexus/logger.rb +188 -0
  106. data/lib/rails_nexus/notifications.rb +105 -0
  107. data/lib/rails_nexus/ransack_config.rb +4 -0
  108. data/lib/rails_nexus/storm_protection.rb +124 -0
  109. data/lib/rails_nexus/version.rb +3 -0
  110. data/lib/rails_nexus.rb +133 -0
  111. data/lib/tasks/rails_nexus.rake +230 -0
  112. data/lib/tasks/rails_nexus_tasks.rake +37 -0
  113. data/rails_nexus.gemspec +36 -0
  114. metadata +252 -0
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNexus
4
+ class Backup < BaseRecord
5
+ self.table_name = "rails_nexus_backups"
6
+
7
+ validates :model_name, presence: true
8
+ validates :status, presence: true, inclusion: { in: %w[running success failed] }
9
+ validates :started_at, presence: true
10
+
11
+ scope :recent, ->(hours = 24) { where("started_at >= ?", hours.hours.ago) }
12
+ scope :successful, -> { where(status: "success") }
13
+ scope :failed, -> { where(status: "failed") }
14
+ scope :running, -> { where(status: "running") }
15
+ scope :by_model, ->(name) { where(model_name: name) }
16
+ scope :latest_first, -> { order(started_at: :desc) }
17
+
18
+ # Start tracking a backup run
19
+ def self.start!(model_name:, triggered_by: "system")
20
+ create!(
21
+ model_name: model_name,
22
+ status: "running",
23
+ triggered_by: triggered_by,
24
+ started_at: Time.current
25
+ )
26
+ end
27
+
28
+ # Mark backup as completed
29
+ def succeed!(file_path: nil, file_size: nil)
30
+ update!(
31
+ status: "success",
32
+ file_path: file_path,
33
+ file_size: file_size,
34
+ duration: (Time.current - started_at).round(2),
35
+ completed_at: Time.current
36
+ )
37
+ end
38
+
39
+ # Mark backup as failed
40
+ def fail!(error_message:)
41
+ update!(
42
+ status: "failed",
43
+ error_message: error_message,
44
+ duration: (Time.current - started_at).round(2),
45
+ completed_at: Time.current
46
+ )
47
+ end
48
+
49
+ # Get success rate for a model over the last N days
50
+ def self.success_rate(model_name:, days: 7)
51
+ records = by_model(model_name).where("started_at >= ?", days.days.ago).where.not(status: "running")
52
+ return 0 if records.empty?
53
+ (records.successful.count.to_f / records.count * 100).round(1)
54
+ end
55
+
56
+ # Get backup health summary
57
+ def self.health_summary(alert_threshold_hours: 24)
58
+ models = RailsNexus.configuration.backup_models || []
59
+ results = models.map do |model_name|
60
+ latest = by_model(model_name).successful.latest_first.first
61
+ age_hours = latest ? ((Time.current - latest.started_at) / 3600).round(1) : nil
62
+
63
+ {
64
+ model_name: model_name,
65
+ last_backup: latest&.started_at,
66
+ age_hours: age_hours,
67
+ status: age_hours.nil? ? "unknown" : (age_hours < alert_threshold_hours ? "healthy" : "stale"),
68
+ success_rate: success_rate(model_name: model_name),
69
+ last_file_size: latest&.file_size
70
+ }
71
+ end
72
+
73
+ {
74
+ healthy: results.all? { |r| r[:status] == "healthy" },
75
+ models: results,
76
+ alerts: results.select { |r| r[:status] == "stale" || r[:status] == "unknown" }
77
+ }
78
+ end
79
+
80
+ # Cleanup old backup records
81
+ def self.cleanup!(retention_days: 30)
82
+ where("started_at < ?", retention_days.days.ago).delete_all
83
+ end
84
+
85
+ # Format file size
86
+ def file_size_human
87
+ return "—" unless file_size
88
+ units = ["B", "KB", "MB", "GB"]
89
+ size = file_size.to_f
90
+ units.each do |unit|
91
+ return "#{size.round(1)} #{unit}" if size < 1024
92
+ size /= 1024
93
+ end
94
+ "#{size.round(1)} TB"
95
+ end
96
+
97
+ # Duration formatted
98
+ def duration_human
99
+ return "—" unless duration
100
+ if duration < 60
101
+ "#{duration.round(1)}s"
102
+ else
103
+ "#{(duration / 60).floor}m #{(duration % 60).round}s"
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNexus
4
+ # Abstract base class for all RailsNexus models.
5
+ # Supports connecting to a separate database via configuration.
6
+ #
7
+ # When NO database config is set, this behaves identically to ApplicationRecord.
8
+ # The host app's default connection is used for all queries.
9
+ #
10
+ # Config options:
11
+ # config.database_url = "mysql2://user:pass@localhost/rails_nexus"
12
+ # config.database_name = "rails_nexus" # looks up in config/database.yml
13
+ # config.database_yml = "config/rails_nexus_database.yml"
14
+ #
15
+ class BaseRecord < ApplicationRecord
16
+ self.abstract_class = true
17
+
18
+ class << self
19
+ # Check if a separate database is configured
20
+ def separate_database?
21
+ config = RailsNexus.configuration
22
+ config.database_url.present? || config.database_name.present? || config.database_yml.present?
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ module RailsNexus
2
+ class Comment < ActiveRecord::Base
3
+ self.table_name = "rails_nexus_comments"
4
+
5
+ belongs_to :logged_exception, counter_cache: :comments_count
6
+
7
+ validates :author, presence: true
8
+ validates :body, presence: true
9
+
10
+ scope :recent, -> { order(created_at: :desc) }
11
+ scope :by_type, ->(type) { where(comment_type: type) }
12
+ end
13
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNexus
4
+ class CronJob < BaseRecord
5
+ self.table_name = "rails_nexus_cron_jobs"
6
+
7
+ STATUSES = %w[pending running success failed].freeze
8
+
9
+ validates :name, presence: true
10
+ validates :status, inclusion: { in: STATUSES }
11
+
12
+ scope :recent, -> { order(created_at: :desc) }
13
+ scope :by_status, ->(status) { where(status: status) }
14
+ scope :failed, -> { by_status("failed") }
15
+ scope :successful, -> { by_status("success") }
16
+ scope :running, -> { by_status("running") }
17
+ scope :last_24h, -> { where("created_at >= ?", 24.hours.ago) }
18
+ scope :last_7d, -> { where("created_at >= ?", 7.days.ago) }
19
+
20
+ # Start a new cron job run
21
+ def self.start!(name, metadata: {})
22
+ create!(
23
+ name: name,
24
+ status: "running",
25
+ started_at: Time.current,
26
+ hostname: Socket.gethostname,
27
+ metadata: metadata
28
+ )
29
+ end
30
+
31
+ # Mark as successful
32
+ def succeed!(output: nil, duration: nil)
33
+ update!(
34
+ status: "success",
35
+ finished_at: Time.current,
36
+ output: output,
37
+ duration: duration || ((finished_at || Time.current) - (started_at || created_at))
38
+ )
39
+ end
40
+
41
+ # Mark as failed
42
+ def fail!(error: nil, duration: nil)
43
+ update!(
44
+ status: "failed",
45
+ finished_at: Time.current,
46
+ error_message: error,
47
+ duration: duration || ((finished_at || Time.current) - (started_at || created_at))
48
+ )
49
+ end
50
+
51
+ # Stats summary
52
+ def self.summary
53
+ {
54
+ total: count,
55
+ successful: successful.count,
56
+ failed: failed.count,
57
+ running: running.count,
58
+ last_run: recent.first,
59
+ failure_rate: count > 0 ? (failed.count.to_f / count * 100).round(1) : 0
60
+ }
61
+ end
62
+
63
+ # Success rate over last N days
64
+ def self.success_rate(days: 7)
65
+ recent_jobs = where("created_at >= ?", days.days.ago)
66
+ total = recent_jobs.count
67
+ return 0 if total == 0
68
+
69
+ (recent_jobs.successful.count.to_f / total * 100).round(1)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNexus
4
+ class DatabaseStat < BaseRecord
5
+ self.table_name = "rails_nexus_database_stats"
6
+
7
+ validates :recorded_at, presence: true
8
+
9
+ scope :recent, ->(hours = 24) { where("recorded_at >= ?", hours.hours.ago) }
10
+ scope :with_tables, -> { where.not(table_name: nil) }
11
+ scope :pool_stats, -> { where(table_name: nil) }
12
+
13
+ # Collect database health snapshot
14
+ def self.collect!
15
+ now = Time.current
16
+ adapter = ActiveRecord::Base.connection.adapter_name.downcase
17
+
18
+ # Connection pool stats
19
+ pool = ActiveRecord::Base.connection_pool
20
+ stats = pool_stats(pool)
21
+ create!(stats.merge(recorded_at: now))
22
+
23
+ # Table stats (MySQL only)
24
+ if adapter.include?("mysql")
25
+ collect_mysql_stats(now)
26
+ elsif adapter.include?("postgresql")
27
+ collect_postgresql_stats(now)
28
+ end
29
+ end
30
+
31
+ # Get pool utilization over time
32
+ def self.pool_utilization(hours: 24)
33
+ pool_stats
34
+ .where("recorded_at >= ?", hours.hours.ago)
35
+ .order(:recorded_at)
36
+ .map { |s| { time: s.recorded_at, utilization: s.utilization, busy: s.busy, idle: s.idle } }
37
+ end
38
+
39
+ # Get slow queries
40
+ def self.slow_queries(limit: 10)
41
+ where.not(slow_queries: nil)
42
+ .order(recorded_at: :desc)
43
+ .limit(1)
44
+ .flat_map(&:slow_queries)
45
+ .first(limit)
46
+ end
47
+
48
+ # Get N+1 patterns
49
+ def self.n1_patterns
50
+ where.not(n1_patterns: nil)
51
+ .order(recorded_at: :desc)
52
+ .limit(1)
53
+ .flat_map(&:n1_patterns)
54
+ end
55
+
56
+ private
57
+
58
+ def self.pool_stats(pool)
59
+ {
60
+ pool_size: pool.size,
61
+ busy: pool.connections.size,
62
+ idle: pool.size - pool.connections.size,
63
+ dead: 0,
64
+ waiting: pool.stat[:waiting] || 0,
65
+ utilization: pool.size > 0 ? (pool.connections.size.to_f / pool.size * 100).round(1) : 0
66
+ }
67
+ end
68
+
69
+ def self.collect_mysql_stats(now)
70
+ conn = ActiveRecord::Base.connection
71
+ tables = conn.execute(<<~SQL)
72
+ SELECT table_name, table_rows, data_length, index_length, data_free
73
+ FROM information_schema.tables
74
+ WHERE table_schema = DATABASE()
75
+ ORDER BY data_length + index_length DESC
76
+ LIMIT 50
77
+ SQL
78
+
79
+ tables.each do |row|
80
+ create!(
81
+ table_name: row["TABLE_NAME"],
82
+ table_rows: row["TABLE_ROWS"],
83
+ table_size: format_size(row["DATA_LENGTH"]),
84
+ index_size: format_size(row["INDEX_LENGTH"]),
85
+ data_free: format_size(row["DATA_FREE"]),
86
+ recorded_at: now
87
+ )
88
+ end
89
+ rescue StandardError => e
90
+ Rails.logger.error("[RailsNexus] Failed to collect MySQL stats: #{e.message}") if defined?(Rails)
91
+ end
92
+
93
+ def self.collect_postgresql_stats(now)
94
+ conn = ActiveRecord::Base.connection
95
+ tables = conn.execute(<<~SQL)
96
+ SELECT schemaname, relname, n_tup_ins, n_tup_upd, n_tup_del,
97
+ pg_size_pretty(pg_total_relation_size(relid)) as total_size,
98
+ pg_size_pretty(pg_indexes_size(relid)) as index_size
99
+ FROM pg_stat_user_tables
100
+ ORDER BY pg_total_relation_size(relid) DESC
101
+ LIMIT 50
102
+ SQL
103
+
104
+ tables.each do |row|
105
+ create!(
106
+ table_name: row["relname"],
107
+ table_rows: (row["n_tup_ins"].to_i + row["n_tup_upd"].to_i - row["n_tup_del"].to_i),
108
+ table_size: row["total_size"],
109
+ index_size: row["index_size"],
110
+ recorded_at: now
111
+ )
112
+ end
113
+ rescue StandardError => e
114
+ Rails.logger.error("[RailsNexus] Failed to collect PostgreSQL stats: #{e.message}") if defined?(Rails)
115
+ end
116
+
117
+ def self.format_size(bytes)
118
+ return "0 B" unless bytes
119
+ units = ["B", "KB", "MB", "GB", "TB"]
120
+ size = bytes.to_f
121
+ units.each do |unit|
122
+ return "#{size.round(1)} #{unit}" if size < 1024
123
+ size /= 1024
124
+ end
125
+ "#{size.round(1)} PB"
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNexus
4
+ class Event < BaseRecord
5
+ self.table_name = "rails_nexus_events"
6
+
7
+ belongs_to :eventable, polymorphic: true, optional: true
8
+
9
+ validates :event_type, presence: true, inclusion: { in: %w[
10
+ breadcrumb storm_start storm_end
11
+ assignment priority_change snooze unsnooze
12
+ mute unmute comment note status_change
13
+ backup_start backup_complete backup_failed
14
+ exception_created exception_resolved
15
+ ] }
16
+
17
+ validates :created_at, presence: true
18
+
19
+ scope :recent, ->(hours = 24) { where("created_at >= ?", hours.hours.ago) }
20
+ scope :by_type, ->(type) { where(event_type: type) }
21
+ scope :breadcrumbs, -> { by_type("breadcrumb") }
22
+ scope :workflow_events, -> { where(event_type: %w[assignment priority_change snooze unsnooze mute unmute comment note]) }
23
+ scope :storm_events, -> { where(event_type: %w[storm_start storm_end]) }
24
+ scope :audit_trail, ->(eventable) { where(eventable: eventable).order(created_at: :desc) }
25
+
26
+ # Record a breadcrumb
27
+ def self.breadcrumb!(message:, metadata: {}, author: nil)
28
+ create!(
29
+ event_type: "breadcrumb",
30
+ message: message,
31
+ metadata: metadata,
32
+ author: author,
33
+ created_at: Time.current
34
+ )
35
+ end
36
+
37
+ # Record a storm event
38
+ def self.storm_start!(threshold:, current_rate:)
39
+ create!(
40
+ event_type: "storm_start",
41
+ message: "Error storm detected: #{current_rate} errors/sec (threshold: #{threshold}/sec)",
42
+ metadata: { threshold: threshold, current_rate: current_rate },
43
+ created_at: Time.current
44
+ )
45
+ end
46
+
47
+ def self.storm_end!(duration_seconds:)
48
+ create!(
49
+ event_type: "storm_end",
50
+ message: "Error storm ended after #{duration_seconds.round(1)}s",
51
+ metadata: { duration_seconds: duration_seconds },
52
+ created_at: Time.current
53
+ )
54
+ end
55
+
56
+ # Record a workflow event
57
+ def self.workflow!(event_type:, eventable:, message:, author: nil, metadata: {})
58
+ create!(
59
+ event_type: event_type,
60
+ eventable: eventable,
61
+ message: message,
62
+ author: author,
63
+ metadata: metadata,
64
+ created_at: Time.current
65
+ )
66
+ end
67
+
68
+ # Get event timeline for an exception
69
+ def self.timeline_for(exception)
70
+ where(eventable: exception).order(created_at: :asc)
71
+ end
72
+
73
+ # Cleanup old events
74
+ def self.cleanup!(retention_days: 30)
75
+ where("created_at < ?", retention_days.days.ago).delete_all
76
+ end
77
+
78
+ # Human-readable event type
79
+ def type_label
80
+ case event_type
81
+ when "breadcrumb" then "🔍 Breadcrumb"
82
+ when "storm_start" then "🌪️ Storm Start"
83
+ when "storm_end" then "☀️ Storm End"
84
+ when "assignment" then "👤 Assigned"
85
+ when "priority_change" then " priority Changed"
86
+ when "snooze" then "😴 Snoozed"
87
+ when "unsnooze" then "⏰ Unsnoozed"
88
+ when "mute" then "🔇 Muted"
89
+ when "unmute" then "🔊 Unmuted"
90
+ when "comment" then "💬 Comment"
91
+ when "note" then "📝 Note"
92
+ when "backup_start" then "💾 Backup Started"
93
+ when "backup_complete" then "✅ Backup Complete"
94
+ when "backup_failed" then "❌ Backup Failed"
95
+ when "exception_created" then "🚨 Exception Created"
96
+ when "exception_resolved" then "✓ Exception Resolved"
97
+ else event_type.humanize
98
+ end
99
+ end
100
+ end
101
+ end