orfeas_lyra 0.6.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +222 -0
- data/LICENSE +21 -0
- data/README.md +1165 -0
- data/Rakefile +728 -0
- data/app/controllers/lyra/application_controller.rb +23 -0
- data/app/controllers/lyra/dashboard_controller.rb +624 -0
- data/app/controllers/lyra/flow_controller.rb +224 -0
- data/app/controllers/lyra/privacy_controller.rb +182 -0
- data/app/views/lyra/dashboard/audit_trail.html.erb +324 -0
- data/app/views/lyra/dashboard/discrepancies.html.erb +125 -0
- data/app/views/lyra/dashboard/event_graph_view.html.erb +525 -0
- data/app/views/lyra/dashboard/heatmap_view.html.erb +155 -0
- data/app/views/lyra/dashboard/index.html.erb +119 -0
- data/app/views/lyra/dashboard/model_overview.html.erb +115 -0
- data/app/views/lyra/dashboard/projections.html.erb +302 -0
- data/app/views/lyra/dashboard/schema.html.erb +283 -0
- data/app/views/lyra/dashboard/schema_history.html.erb +78 -0
- data/app/views/lyra/dashboard/schema_version.html.erb +340 -0
- data/app/views/lyra/dashboard/verification.html.erb +370 -0
- data/app/views/lyra/flow/crud_mapping.html.erb +125 -0
- data/app/views/lyra/flow/timeline.html.erb +260 -0
- data/app/views/lyra/privacy/pii_detection.html.erb +148 -0
- data/app/views/lyra/privacy/policy.html.erb +188 -0
- data/app/workflows/es_async_mode_workflow.rb +80 -0
- data/app/workflows/es_sync_mode_workflow.rb +64 -0
- data/app/workflows/hijack_mode_workflow.rb +54 -0
- data/app/workflows/lifecycle_workflow.rb +43 -0
- data/app/workflows/monitor_mode_workflow.rb +39 -0
- data/config/privacy_policies.rb +273 -0
- data/config/routes.rb +48 -0
- data/lib/lyra/aggregate.rb +131 -0
- data/lib/lyra/associations/event_aware.rb +225 -0
- data/lib/lyra/command.rb +81 -0
- data/lib/lyra/command_handler.rb +155 -0
- data/lib/lyra/configuration.rb +124 -0
- data/lib/lyra/consistency/read_your_writes.rb +91 -0
- data/lib/lyra/correlation.rb +144 -0
- data/lib/lyra/dual_view.rb +231 -0
- data/lib/lyra/engine.rb +67 -0
- data/lib/lyra/event.rb +71 -0
- data/lib/lyra/event_analyzer.rb +135 -0
- data/lib/lyra/event_flow.rb +449 -0
- data/lib/lyra/event_mapper.rb +106 -0
- data/lib/lyra/event_store_adapter.rb +72 -0
- data/lib/lyra/id_generator.rb +137 -0
- data/lib/lyra/interceptors/association_interceptor.rb +169 -0
- data/lib/lyra/interceptors/crud_interceptor.rb +543 -0
- data/lib/lyra/privacy/gdpr_compliance.rb +161 -0
- data/lib/lyra/privacy/pii_detector.rb +85 -0
- data/lib/lyra/privacy/pii_masker.rb +66 -0
- data/lib/lyra/privacy/policy_integration.rb +253 -0
- data/lib/lyra/projection.rb +94 -0
- data/lib/lyra/projections/async_projection_job.rb +63 -0
- data/lib/lyra/projections/cached_projection.rb +322 -0
- data/lib/lyra/projections/cached_relation.rb +757 -0
- data/lib/lyra/projections/event_store_reader.rb +127 -0
- data/lib/lyra/projections/model_projection.rb +143 -0
- data/lib/lyra/schema/diff.rb +331 -0
- data/lib/lyra/schema/event_class_registrar.rb +63 -0
- data/lib/lyra/schema/generator.rb +190 -0
- data/lib/lyra/schema/reporter.rb +188 -0
- data/lib/lyra/schema/store.rb +156 -0
- data/lib/lyra/schema/validator.rb +100 -0
- data/lib/lyra/strict_data_access.rb +363 -0
- data/lib/lyra/verification/crud_lifecycle_workflow.rb +456 -0
- data/lib/lyra/verification/workflow_generator.rb +540 -0
- data/lib/lyra/version.rb +3 -0
- data/lib/lyra/visualization/activity_heatmap.rb +215 -0
- data/lib/lyra/visualization/event_graph.rb +310 -0
- data/lib/lyra/visualization/timeline.rb +398 -0
- data/lib/lyra.rb +150 -0
- data/lib/tasks/dist.rake +391 -0
- data/lib/tasks/gems.rake +185 -0
- data/lib/tasks/lyra_schema.rake +231 -0
- data/lib/tasks/lyra_workflows.rake +452 -0
- data/lib/tasks/public_release.rake +351 -0
- data/lib/tasks/stats.rake +175 -0
- data/lib/tasks/testbed.rake +479 -0
- data/lib/tasks/version.rake +159 -0
- metadata +221 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.lyra-projections { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
3
|
+
.lyra-projections h1 { color: #1a1a2e; border-bottom: 2px solid #4a4e69; padding-bottom: 10px; }
|
|
4
|
+
.lyra-projections h2 { color: #4a4e69; margin-top: 30px; }
|
|
5
|
+
.lyra-projections .back-link { display: inline-block; margin-bottom: 20px; color: #4a4e69; text-decoration: none; }
|
|
6
|
+
.lyra-projections .back-link:hover { text-decoration: underline; }
|
|
7
|
+
.lyra-projections .config-card { background: #fff; border: 1px solid #dee2e6; border-radius: 8px; margin-bottom: 20px; overflow: hidden; }
|
|
8
|
+
.lyra-projections .config-header { background: #f8f9fa; padding: 15px 20px; border-bottom: 1px solid #dee2e6; }
|
|
9
|
+
.lyra-projections .config-header h3 { margin: 0; font-size: 16px; color: #1a1a2e; }
|
|
10
|
+
.lyra-projections .config-body { padding: 20px; }
|
|
11
|
+
.lyra-projections .config-table { width: 100%; border-collapse: collapse; }
|
|
12
|
+
.lyra-projections .config-table th { text-align: left; padding: 10px 15px; background: #f8f9fa; border-bottom: 1px solid #dee2e6; font-weight: 600; color: #495057; width: 200px; }
|
|
13
|
+
.lyra-projections .config-table td { padding: 10px 15px; border-bottom: 1px solid #eee; }
|
|
14
|
+
.lyra-projections .config-table tr:last-child td { border-bottom: none; }
|
|
15
|
+
.lyra-projections .badge { display: inline-block; padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: 600; }
|
|
16
|
+
.lyra-projections .badge-mode-monitor { background: #d4edda; color: #155724; }
|
|
17
|
+
.lyra-projections .badge-mode-hijack { background: #fff3cd; color: #856404; }
|
|
18
|
+
.lyra-projections .badge-mode-event_sourcing { background: #cce5ff; color: #004085; }
|
|
19
|
+
.lyra-projections .badge-mode-disabled { background: #f8d7da; color: #721c24; }
|
|
20
|
+
.lyra-projections .badge-sync { background: #d4edda; color: #155724; }
|
|
21
|
+
.lyra-projections .badge-async { background: #fff3cd; color: #856404; }
|
|
22
|
+
.lyra-projections .badge-disabled { background: #f8d7da; color: #721c24; }
|
|
23
|
+
.lyra-projections .badge-inline { background: #e2e3e5; color: #383d41; }
|
|
24
|
+
.lyra-projections .summary-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 20px; margin: 20px 0; }
|
|
25
|
+
.lyra-projections .summary-card { background: #fff; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; text-align: center; }
|
|
26
|
+
.lyra-projections .summary-card .value { font-size: 36px; font-weight: 700; color: #1a1a2e; }
|
|
27
|
+
.lyra-projections .summary-card .label { color: #6c757d; font-size: 14px; margin-top: 5px; }
|
|
28
|
+
.lyra-projections .projection-type { background: #fff; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; margin-bottom: 15px; }
|
|
29
|
+
.lyra-projections .projection-type h4 { margin: 0 0 10px 0; color: #1a1a2e; font-size: 16px; display: flex; align-items: center; gap: 10px; }
|
|
30
|
+
.lyra-projections .projection-type .description { color: #495057; font-size: 14px; margin-bottom: 10px; }
|
|
31
|
+
.lyra-projections .projection-type .use-case { color: #6c757d; font-size: 13px; font-style: italic; }
|
|
32
|
+
.lyra-projections .projection-type .meta { margin-top: 10px; display: flex; gap: 15px; }
|
|
33
|
+
.lyra-projections .projection-type .meta-item { font-size: 12px; color: #6c757d; }
|
|
34
|
+
.lyra-projections .projection-type .meta-item strong { color: #495057; }
|
|
35
|
+
.lyra-projections .model-stat-card { background: #fff; border: 1px solid #dee2e6; border-radius: 8px; margin-bottom: 15px; overflow: hidden; }
|
|
36
|
+
.lyra-projections .model-stat-header { background: #f8f9fa; padding: 15px 20px; border-bottom: 1px solid #dee2e6; display: flex; justify-content: space-between; align-items: center; }
|
|
37
|
+
.lyra-projections .model-stat-header h4 { margin: 0; font-size: 16px; color: #1a1a2e; }
|
|
38
|
+
.lyra-projections .model-stat-header .last-event { font-size: 12px; color: #6c757d; }
|
|
39
|
+
.lyra-projections .model-stat-body { padding: 20px; }
|
|
40
|
+
.lyra-projections .stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 15px; margin-bottom: 15px; }
|
|
41
|
+
.lyra-projections .stat-item { text-align: center; padding: 10px; background: #f8f9fa; border-radius: 6px; }
|
|
42
|
+
.lyra-projections .stat-item .value { font-size: 24px; font-weight: 700; }
|
|
43
|
+
.lyra-projections .stat-item .label { font-size: 11px; color: #6c757d; text-transform: uppercase; }
|
|
44
|
+
.lyra-projections .stat-item.created .value { color: #28a745; }
|
|
45
|
+
.lyra-projections .stat-item.updated .value { color: #007bff; }
|
|
46
|
+
.lyra-projections .stat-item.destroyed .value { color: #dc3545; }
|
|
47
|
+
.lyra-projections .config-list { font-size: 13px; color: #6c757d; }
|
|
48
|
+
.lyra-projections .config-list dt { font-weight: 600; color: #495057; margin-top: 8px; }
|
|
49
|
+
.lyra-projections .config-list dd { margin: 0 0 0 15px; }
|
|
50
|
+
.lyra-projections .mode-desc { color: #6c757d; font-size: 13px; margin-top: 5px; }
|
|
51
|
+
.lyra-projections .section-desc { color: #6c757d; font-size: 13px; margin-bottom: 15px; }
|
|
52
|
+
.lyra-projections .icon { width: 20px; height: 20px; display: inline-block; }
|
|
53
|
+
</style>
|
|
54
|
+
|
|
55
|
+
<div class="lyra-projections">
|
|
56
|
+
<%= link_to "← Back to Dashboard".html_safe, dashboard_path, class: "back-link" %>
|
|
57
|
+
|
|
58
|
+
<h1>Projections & Configuration</h1>
|
|
59
|
+
<p class="section-desc">
|
|
60
|
+
Projections are the mechanism that rebuilds read models (database state) from events.
|
|
61
|
+
In event sourcing, the event log is the source of truth, and projections derive the current state.
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<h2 style="margin-top: 0;">Current Configuration</h2>
|
|
65
|
+
<p class="section-desc">
|
|
66
|
+
Active Lyra mode and projection settings that control how events are processed and projected.
|
|
67
|
+
</p>
|
|
68
|
+
|
|
69
|
+
<div class="config-card">
|
|
70
|
+
<div class="config-header">
|
|
71
|
+
<h3>Lyra Settings</h3>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="config-body">
|
|
74
|
+
<table class="config-table">
|
|
75
|
+
<tr>
|
|
76
|
+
<th>Operating Mode</th>
|
|
77
|
+
<td>
|
|
78
|
+
<span class="badge badge-mode-<%= @mode %>"><%= @mode.to_s.titleize %></span>
|
|
79
|
+
<p class="mode-desc">
|
|
80
|
+
<% case @mode.to_sym
|
|
81
|
+
when :monitor %>
|
|
82
|
+
Events are recorded after database saves. Database is the source of truth. Projections are not used for writes.
|
|
83
|
+
<% when :hijack %>
|
|
84
|
+
Events are recorded before database saves. Allows intercepting and modifying operations before persistence.
|
|
85
|
+
<% when :event_sourcing %>
|
|
86
|
+
Events are the source of truth. Database tables are updated via projections only. No direct ActiveRecord saves.
|
|
87
|
+
<% when :disabled %>
|
|
88
|
+
Event tracking is disabled. No events are recorded and projections are inactive.
|
|
89
|
+
<% end %>
|
|
90
|
+
</p>
|
|
91
|
+
</td>
|
|
92
|
+
</tr>
|
|
93
|
+
<tr>
|
|
94
|
+
<th>Projection Mode</th>
|
|
95
|
+
<td>
|
|
96
|
+
<span class="badge badge-<%= @projection_mode %>"><%= @projection_mode.to_s.titleize %></span>
|
|
97
|
+
<% if @projection_mode.to_sym == :async && @async_inline %>
|
|
98
|
+
<span class="badge badge-inline" style="margin-left: 8px;">Inline (Test Mode)</span>
|
|
99
|
+
<% end %>
|
|
100
|
+
<p class="mode-desc">
|
|
101
|
+
<% case @projection_mode.to_sym
|
|
102
|
+
when :sync %>
|
|
103
|
+
Projections run synchronously after each event. Immediate consistency - reads always see latest writes.
|
|
104
|
+
Trade-off: Slower write operations as projections must complete before response.
|
|
105
|
+
<% when :async %>
|
|
106
|
+
Projections run asynchronously via ActiveJob (<code>lyra_projections</code> queue).
|
|
107
|
+
Eventual consistency - reads may be briefly stale. Trade-off: Faster writes, requires job infrastructure.
|
|
108
|
+
<% if @async_inline %>
|
|
109
|
+
<br><strong>Note:</strong> Currently running inline for testing (async_projections_inline = true).
|
|
110
|
+
<% end %>
|
|
111
|
+
<% when :disabled %>
|
|
112
|
+
Projections are disabled. Events are stored but read models are not updated automatically.
|
|
113
|
+
Use <code>StateProjection.rebuild_state</code> for manual replay when needed.
|
|
114
|
+
<% end %>
|
|
115
|
+
</p>
|
|
116
|
+
</td>
|
|
117
|
+
</tr>
|
|
118
|
+
<tr>
|
|
119
|
+
<th>Strict Projections</th>
|
|
120
|
+
<td>
|
|
121
|
+
<span class="badge <%= @strict_projections ? 'badge-sync' : 'badge-disabled' %>">
|
|
122
|
+
<%= @strict_projections ? 'Enabled' : 'Disabled' %>
|
|
123
|
+
</span>
|
|
124
|
+
<p class="mode-desc">
|
|
125
|
+
<% if @strict_projections %>
|
|
126
|
+
Projection errors raise exceptions and halt processing. Use in development to catch issues early.
|
|
127
|
+
<% else %>
|
|
128
|
+
Projection errors are logged but do not halt processing. More resilient in production.
|
|
129
|
+
<% end %>
|
|
130
|
+
</p>
|
|
131
|
+
</td>
|
|
132
|
+
</tr>
|
|
133
|
+
<tr>
|
|
134
|
+
<th>Monitored Models</th>
|
|
135
|
+
<td><%= @monitored_models.count %> models with <code>monitor_with_lyra</code></td>
|
|
136
|
+
</tr>
|
|
137
|
+
</table>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<% unless user_tracking_configured? %>
|
|
142
|
+
<h2>User Tracking Setup</h2>
|
|
143
|
+
<p class="section-desc">
|
|
144
|
+
Lyra captures user_id for audit trails via Rails' CurrentAttributes. Configure your app to track who made changes.
|
|
145
|
+
</p>
|
|
146
|
+
<div class="config-card">
|
|
147
|
+
<div class="config-header">
|
|
148
|
+
<h3>Required Setup for User Tracking</h3>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="config-body">
|
|
151
|
+
<p style="margin: 0 0 15px 0; color: #495057;">
|
|
152
|
+
Lyra reads <code>Current.user</code> to capture who made each change. Add this to your application:
|
|
153
|
+
</p>
|
|
154
|
+
<pre style="background: #f8f9fa; padding: 15px; border-radius: 6px; font-size: 13px; overflow-x: auto; margin: 0 0 15px 0;"><code># app/models/current.rb
|
|
155
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
156
|
+
attribute :user
|
|
157
|
+
attribute :request_id
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# app/controllers/application_controller.rb
|
|
161
|
+
class ApplicationController < ActionController::Base
|
|
162
|
+
before_action :set_current_user
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def set_current_user
|
|
167
|
+
Current.user = current_user # your auth method
|
|
168
|
+
Current.request_id = request.request_id
|
|
169
|
+
end
|
|
170
|
+
end</code></pre>
|
|
171
|
+
<p style="margin: 0; color: #6c757d; font-size: 13px;">
|
|
172
|
+
Once configured, every event will include the user_id in metadata, shown in audit trails with the user's name (e.g., "by John Doe (Admin)").
|
|
173
|
+
</p>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<% end %>
|
|
177
|
+
|
|
178
|
+
<h2>Projection Types</h2>
|
|
179
|
+
<p class="section-desc">
|
|
180
|
+
Lyra provides several projection types for different use cases. Each projection transforms
|
|
181
|
+
events into a different view of the data.
|
|
182
|
+
</p>
|
|
183
|
+
|
|
184
|
+
<% @projection_types.each do |proj| %>
|
|
185
|
+
<div class="projection-type">
|
|
186
|
+
<h4>
|
|
187
|
+
<span class="badge badge-mode-event_sourcing"><%= proj[:name] %></span>
|
|
188
|
+
</h4>
|
|
189
|
+
<p class="description"><%= proj[:description] %></p>
|
|
190
|
+
<p class="use-case">Use case: <%= proj[:use_case] %></p>
|
|
191
|
+
<% if proj[:queue] || proj[:retry_attempts] %>
|
|
192
|
+
<div class="meta">
|
|
193
|
+
<% if proj[:queue] %>
|
|
194
|
+
<span class="meta-item"><strong>Queue:</strong> <%= proj[:queue] %></span>
|
|
195
|
+
<% end %>
|
|
196
|
+
<% if proj[:retry_attempts] %>
|
|
197
|
+
<span class="meta-item"><strong>Retry attempts:</strong> <%= proj[:retry_attempts] %></span>
|
|
198
|
+
<% end %>
|
|
199
|
+
</div>
|
|
200
|
+
<% end %>
|
|
201
|
+
</div>
|
|
202
|
+
<% end %>
|
|
203
|
+
|
|
204
|
+
<h2>Model Statistics</h2>
|
|
205
|
+
<p class="section-desc">
|
|
206
|
+
Event counts, operation breakdown, and projection status for each monitored model.
|
|
207
|
+
The operation breakdown shows how many create, update, and destroy events have been recorded.
|
|
208
|
+
</p>
|
|
209
|
+
|
|
210
|
+
<%
|
|
211
|
+
total_events = @model_stats.values.sum { |s| s[:events] }
|
|
212
|
+
total_records = @model_stats.values.sum { |s| s[:records] }
|
|
213
|
+
total_created = @model_stats.values.sum { |s| s[:created] }
|
|
214
|
+
total_updated = @model_stats.values.sum { |s| s[:updated] }
|
|
215
|
+
total_destroyed = @model_stats.values.sum { |s| s[:destroyed] }
|
|
216
|
+
%>
|
|
217
|
+
<div class="summary-cards">
|
|
218
|
+
<div class="summary-card">
|
|
219
|
+
<div class="value"><%= @monitored_models.count %></div>
|
|
220
|
+
<div class="label">Monitored Models</div>
|
|
221
|
+
</div>
|
|
222
|
+
<div class="summary-card">
|
|
223
|
+
<div class="value"><%= total_records %></div>
|
|
224
|
+
<div class="label">Total Records</div>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="summary-card">
|
|
227
|
+
<div class="value"><%= total_events %></div>
|
|
228
|
+
<div class="label">Total Events</div>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="summary-card">
|
|
231
|
+
<div class="value" style="color: #28a745;"><%= total_created %></div>
|
|
232
|
+
<div class="label">Created</div>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="summary-card">
|
|
235
|
+
<div class="value" style="color: #007bff;"><%= total_updated %></div>
|
|
236
|
+
<div class="label">Updated</div>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="summary-card">
|
|
239
|
+
<div class="value" style="color: #dc3545;"><%= total_destroyed %></div>
|
|
240
|
+
<div class="label">Destroyed</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<% @model_stats.each do |model_name, stats| %>
|
|
245
|
+
<div class="model-stat-card">
|
|
246
|
+
<div class="model-stat-header">
|
|
247
|
+
<h4><%= model_name %></h4>
|
|
248
|
+
<% if stats[:last_event_at] %>
|
|
249
|
+
<span class="last-event">Last event: <%= stats[:last_event_at].is_a?(String) ? stats[:last_event_at] : stats[:last_event_at]&.strftime("%Y-%m-%d %H:%M:%S") %></span>
|
|
250
|
+
<% end %>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="model-stat-body">
|
|
253
|
+
<div class="stat-grid">
|
|
254
|
+
<div class="stat-item">
|
|
255
|
+
<div class="value"><%= stats[:records] %></div>
|
|
256
|
+
<div class="label">Records</div>
|
|
257
|
+
</div>
|
|
258
|
+
<div class="stat-item">
|
|
259
|
+
<div class="value"><%= stats[:events] %></div>
|
|
260
|
+
<div class="label">Events</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="stat-item">
|
|
263
|
+
<div class="value"><%= stats[:avg_events_per_record] %></div>
|
|
264
|
+
<div class="label">Avg/Record</div>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="stat-item created">
|
|
267
|
+
<div class="value"><%= stats[:created] %></div>
|
|
268
|
+
<div class="label">Created</div>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="stat-item updated">
|
|
271
|
+
<div class="value"><%= stats[:updated] %></div>
|
|
272
|
+
<div class="label">Updated</div>
|
|
273
|
+
</div>
|
|
274
|
+
<div class="stat-item destroyed">
|
|
275
|
+
<div class="value"><%= stats[:destroyed] %></div>
|
|
276
|
+
<div class="label">Destroyed</div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<% config = @model_configs[model_name] %>
|
|
281
|
+
<% if config %>
|
|
282
|
+
<dl class="config-list">
|
|
283
|
+
<dt>Event Prefix</dt>
|
|
284
|
+
<dd><code><%= config[:event_prefix] %></code></dd>
|
|
285
|
+
<% if config[:aggregate_class] %>
|
|
286
|
+
<dt>Aggregate Class</dt>
|
|
287
|
+
<dd><code><%= config[:aggregate_class] %></code></dd>
|
|
288
|
+
<% end %>
|
|
289
|
+
<% if config[:command_handler] %>
|
|
290
|
+
<dt>Command Handler</dt>
|
|
291
|
+
<dd><code><%= config[:command_handler] %></code></dd>
|
|
292
|
+
<% end %>
|
|
293
|
+
<% if config[:privacy_policy] %>
|
|
294
|
+
<dt>Privacy Policy</dt>
|
|
295
|
+
<dd><code><%= config[:privacy_policy] %></code></dd>
|
|
296
|
+
<% end %>
|
|
297
|
+
</dl>
|
|
298
|
+
<% end %>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
<% end %>
|
|
302
|
+
</div>
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.lyra-schema { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
3
|
+
.lyra-schema h1 { color: #1a1a2e; border-bottom: 2px solid #4a4e69; padding-bottom: 10px; }
|
|
4
|
+
.lyra-schema h2 { color: #4a4e69; margin-top: 30px; }
|
|
5
|
+
.lyra-schema .back-link { display: inline-block; margin-bottom: 20px; color: #4a4e69; text-decoration: none; }
|
|
6
|
+
.lyra-schema .back-link:hover { text-decoration: underline; }
|
|
7
|
+
.lyra-schema .section-desc { color: #6c757d; font-size: 13px; margin-bottom: 15px; }
|
|
8
|
+
.lyra-schema .config-card { background: #fff; border: 1px solid #dee2e6; border-radius: 8px; margin-bottom: 20px; overflow: hidden; }
|
|
9
|
+
.lyra-schema .config-header { background: #f8f9fa; padding: 15px 20px; border-bottom: 1px solid #dee2e6; display: flex; justify-content: space-between; align-items: center; }
|
|
10
|
+
.lyra-schema .config-header h3 { margin: 0; font-size: 16px; color: #1a1a2e; }
|
|
11
|
+
.lyra-schema .config-body { padding: 20px; }
|
|
12
|
+
.lyra-schema .config-table { width: 100%; border-collapse: collapse; }
|
|
13
|
+
.lyra-schema .config-table th { text-align: left; padding: 10px 15px; background: #f8f9fa; border-bottom: 1px solid #dee2e6; font-weight: 600; color: #495057; width: 200px; }
|
|
14
|
+
.lyra-schema .config-table td { padding: 10px 15px; border-bottom: 1px solid #eee; }
|
|
15
|
+
.lyra-schema .config-table tr:last-child td { border-bottom: none; }
|
|
16
|
+
.lyra-schema .badge { display: inline-block; padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: 600; }
|
|
17
|
+
.lyra-schema .badge-success { background: #d4edda; color: #155724; }
|
|
18
|
+
.lyra-schema .badge-warning { background: #fff3cd; color: #856404; }
|
|
19
|
+
.lyra-schema .badge-info { background: #cce5ff; color: #004085; }
|
|
20
|
+
.lyra-schema .badge-secondary { background: #e2e3e5; color: #383d41; }
|
|
21
|
+
.lyra-schema .badge-danger { background: #f8d7da; color: #721c24; }
|
|
22
|
+
.lyra-schema .summary-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 20px; margin: 20px 0; }
|
|
23
|
+
.lyra-schema .summary-card { background: #fff; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; text-align: center; }
|
|
24
|
+
.lyra-schema .summary-card .value { font-size: 24px; font-weight: 700; color: #1a1a2e; word-break: break-all; }
|
|
25
|
+
.lyra-schema .summary-card .value.small { font-size: 14px; font-family: monospace; }
|
|
26
|
+
.lyra-schema .summary-card .label { color: #6c757d; font-size: 14px; margin-top: 5px; }
|
|
27
|
+
.lyra-schema .model-card { background: #fff; border: 1px solid #dee2e6; border-radius: 8px; margin-bottom: 15px; overflow: hidden; }
|
|
28
|
+
.lyra-schema .model-header { background: #f8f9fa; padding: 15px 20px; border-bottom: 1px solid #dee2e6; display: flex; justify-content: space-between; align-items: center; }
|
|
29
|
+
.lyra-schema .model-header h4 { margin: 0; font-size: 16px; color: #1a1a2e; }
|
|
30
|
+
.lyra-schema .model-body { padding: 20px; }
|
|
31
|
+
.lyra-schema .field-list { margin: 0; padding: 0; list-style: none; }
|
|
32
|
+
.lyra-schema .field-list li { padding: 8px 12px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
|
33
|
+
.lyra-schema .field-list li:last-child { border-bottom: none; }
|
|
34
|
+
.lyra-schema .field-name { font-family: monospace; font-size: 13px; color: #495057; }
|
|
35
|
+
.lyra-schema .field-type { font-size: 12px; color: #6c757d; }
|
|
36
|
+
.lyra-schema .pii-badge { background: #f8d7da; color: #721c24; font-size: 10px; padding: 2px 6px; border-radius: 3px; margin-left: 8px; }
|
|
37
|
+
.lyra-schema .event-prefix { font-family: monospace; font-size: 13px; color: #6c757d; }
|
|
38
|
+
.lyra-schema .change-list { margin: 15px 0; padding: 0; list-style: none; }
|
|
39
|
+
.lyra-schema .change-item { padding: 12px 15px; border-left: 4px solid #ffc107; background: #fffbea; margin-bottom: 10px; border-radius: 0 4px 4px 0; }
|
|
40
|
+
.lyra-schema .change-item.added { border-left-color: #28a745; background: #f0fff4; }
|
|
41
|
+
.lyra-schema .change-item.removed { border-left-color: #dc3545; background: #fff5f5; }
|
|
42
|
+
.lyra-schema .change-item.modified { border-left-color: #007bff; background: #f0f7ff; }
|
|
43
|
+
.lyra-schema .change-type { font-weight: 600; font-size: 12px; text-transform: uppercase; margin-bottom: 4px; }
|
|
44
|
+
.lyra-schema .change-detail { font-size: 13px; color: #495057; }
|
|
45
|
+
.lyra-schema .no-changes { color: #28a745; font-style: italic; }
|
|
46
|
+
.lyra-schema .no-schema { text-align: center; padding: 40px; color: #6c757d; }
|
|
47
|
+
.lyra-schema .no-schema h3 { color: #495057; }
|
|
48
|
+
.lyra-schema code { background: #f8f9fa; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
|
49
|
+
.lyra-schema .alert { padding: 15px 20px; border-radius: 6px; margin-bottom: 20px; }
|
|
50
|
+
.lyra-schema .alert-warning { background: #fff3cd; border: 1px solid #ffc107; color: #856404; }
|
|
51
|
+
.lyra-schema .alert-info { background: #cce5ff; border: 1px solid #007bff; color: #004085; }
|
|
52
|
+
</style>
|
|
53
|
+
|
|
54
|
+
<div class="lyra-schema">
|
|
55
|
+
<%= link_to "← Back to Dashboard".html_safe, dashboard_path, class: "back-link" %>
|
|
56
|
+
|
|
57
|
+
<h1>Schema Registry</h1>
|
|
58
|
+
<p class="section-desc">
|
|
59
|
+
The schema registry tracks your event structure<%= ", model configurations, and PII field mappings" if pam_dsl_available? %><%= " and model configurations" unless pam_dsl_available? %>.
|
|
60
|
+
Version your schema to detect breaking changes and maintain event compatibility.
|
|
61
|
+
</p>
|
|
62
|
+
|
|
63
|
+
<% if @version.nil? %>
|
|
64
|
+
<div class="no-schema">
|
|
65
|
+
<h3>No Schema Version Committed</h3>
|
|
66
|
+
<p>Run <code>rake lyra:schema:commit</code> to create your first schema version.</p>
|
|
67
|
+
<p>This will snapshot your current model configurations and event structures.</p>
|
|
68
|
+
</div>
|
|
69
|
+
<% else %>
|
|
70
|
+
<% if @has_pending_changes %>
|
|
71
|
+
<div class="alert alert-warning">
|
|
72
|
+
<strong>Pending Changes Detected</strong> —
|
|
73
|
+
Your schema has uncommitted changes. Run <code>rake lyra:schema:commit</code> to version them.
|
|
74
|
+
</div>
|
|
75
|
+
<% end %>
|
|
76
|
+
|
|
77
|
+
<h2 style="margin-top: 0;">Schema Version</h2>
|
|
78
|
+
<p class="section-desc">
|
|
79
|
+
Current committed schema version and metadata.
|
|
80
|
+
</p>
|
|
81
|
+
|
|
82
|
+
<div class="summary-cards">
|
|
83
|
+
<div class="summary-card">
|
|
84
|
+
<div class="value"><%= @version %></div>
|
|
85
|
+
<div class="label">Version</div>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="summary-card">
|
|
88
|
+
<div class="value small"><%= @fingerprint&.first(12) %>...</div>
|
|
89
|
+
<div class="label">Fingerprint</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="summary-card">
|
|
92
|
+
<div class="value small"><%= @created_at&.is_a?(String) ? @created_at : @created_at&.strftime("%Y-%m-%d") %></div>
|
|
93
|
+
<div class="label">Created At</div>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="summary-card">
|
|
96
|
+
<div class="value small"><%= @lyra_version || "unknown" %></div>
|
|
97
|
+
<div class="label">Lyra Version</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<h2>Configuration</h2>
|
|
102
|
+
<p class="section-desc">
|
|
103
|
+
Global Lyra configuration captured in this schema version.
|
|
104
|
+
</p>
|
|
105
|
+
|
|
106
|
+
<div class="config-card">
|
|
107
|
+
<div class="config-header">
|
|
108
|
+
<h3>Lyra Settings</h3>
|
|
109
|
+
<span class="badge badge-info">v<%= @version %></span>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="config-body">
|
|
112
|
+
<table class="config-table">
|
|
113
|
+
<tr>
|
|
114
|
+
<th>Operating Mode</th>
|
|
115
|
+
<td>
|
|
116
|
+
<span class="badge badge-<%= @configuration[:mode] == 'event_sourcing' ? 'info' : (@configuration[:mode] == 'hijack' ? 'warning' : 'success') %>">
|
|
117
|
+
<%= @configuration[:mode]&.to_s&.titleize || "Not set" %>
|
|
118
|
+
</span>
|
|
119
|
+
</td>
|
|
120
|
+
</tr>
|
|
121
|
+
<tr>
|
|
122
|
+
<th>Projection Mode</th>
|
|
123
|
+
<td>
|
|
124
|
+
<span class="badge badge-secondary">
|
|
125
|
+
<%= @configuration[:projection_mode]&.to_s&.titleize || "Not set" %>
|
|
126
|
+
</span>
|
|
127
|
+
</td>
|
|
128
|
+
</tr>
|
|
129
|
+
<tr>
|
|
130
|
+
<th>Strict Projections</th>
|
|
131
|
+
<td>
|
|
132
|
+
<span class="badge <%= @configuration[:strict_projections] ? 'badge-warning' : 'badge-secondary' %>">
|
|
133
|
+
<%= @configuration[:strict_projections] ? "Enabled" : "Disabled" %>
|
|
134
|
+
</span>
|
|
135
|
+
</td>
|
|
136
|
+
</tr>
|
|
137
|
+
<tr>
|
|
138
|
+
<th>Monitored Models</th>
|
|
139
|
+
<td><%= @models.keys.count %> models</td>
|
|
140
|
+
</tr>
|
|
141
|
+
</table>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<h2>Model Schemas</h2>
|
|
146
|
+
<p class="section-desc">
|
|
147
|
+
Each monitored model's field definitions<%= " and types" unless pam_dsl_available? %><%= ", types, and PII classifications" if pam_dsl_available? %> as captured in the schema.
|
|
148
|
+
</p>
|
|
149
|
+
|
|
150
|
+
<% @models.each do |model_name, model_data| %>
|
|
151
|
+
<div class="model-card">
|
|
152
|
+
<div class="model-header">
|
|
153
|
+
<h4><%= model_name %></h4>
|
|
154
|
+
<span class="event-prefix">Events: <%= model_data[:event_prefix] || model_name %>.*</span>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="model-body">
|
|
157
|
+
<% columns = model_data[:columns] || model_data["columns"] || {} %>
|
|
158
|
+
<% pii_columns = columns.select { |_, c| c[:pii] || c["pii"] } %>
|
|
159
|
+
|
|
160
|
+
<% if columns.any? %>
|
|
161
|
+
<ul class="field-list">
|
|
162
|
+
<% columns.each do |col_name, col_info| %>
|
|
163
|
+
<li>
|
|
164
|
+
<span class="field-name">
|
|
165
|
+
<%= col_name %>
|
|
166
|
+
<% if pam_dsl_available? && (col_info[:pii] || col_info["pii"]) %>
|
|
167
|
+
<span class="pii-badge"><%= col_info[:pii_type] || col_info["pii_type"] || "PII" %></span>
|
|
168
|
+
<% end %>
|
|
169
|
+
<% if col_info[:primary_key] || col_info["primary_key"] %>
|
|
170
|
+
<span class="pii-badge" style="background: #cce5ff; color: #004085;">PK</span>
|
|
171
|
+
<% end %>
|
|
172
|
+
</span>
|
|
173
|
+
<span class="field-type">
|
|
174
|
+
<%= col_info[:type] || col_info["type"] || "unknown" %>
|
|
175
|
+
<% if col_info[:nullable] == false || col_info["nullable"] == false %>
|
|
176
|
+
<span style="color: #dc3545;">NOT NULL</span>
|
|
177
|
+
<% end %>
|
|
178
|
+
</span>
|
|
179
|
+
</li>
|
|
180
|
+
<% end %>
|
|
181
|
+
</ul>
|
|
182
|
+
<% else %>
|
|
183
|
+
<p style="color: #6c757d; font-style: italic; margin: 0;">No columns captured in schema</p>
|
|
184
|
+
<% end %>
|
|
185
|
+
|
|
186
|
+
<% if pam_dsl_available? && pii_columns.any? %>
|
|
187
|
+
<p style="margin-top: 15px; margin-bottom: 0; font-size: 12px; color: #721c24;">
|
|
188
|
+
<strong>PII Fields:</strong> <%= pii_columns.keys.join(", ") %>
|
|
189
|
+
</p>
|
|
190
|
+
<% end %>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
<% end %>
|
|
194
|
+
|
|
195
|
+
<% if @models.empty? %>
|
|
196
|
+
<div class="alert alert-info">
|
|
197
|
+
No models captured in the current schema. Add <code>monitor_with_lyra</code> to your models and re-commit the schema.
|
|
198
|
+
</div>
|
|
199
|
+
<% end %>
|
|
200
|
+
|
|
201
|
+
<h2>Pending Changes</h2>
|
|
202
|
+
<p class="section-desc">
|
|
203
|
+
Differences between the committed schema and current configuration.
|
|
204
|
+
Commit changes to maintain an accurate schema history.
|
|
205
|
+
</p>
|
|
206
|
+
|
|
207
|
+
<% if @pending_changes.any? %>
|
|
208
|
+
<ul class="change-list">
|
|
209
|
+
<% @pending_changes.each do |change| %>
|
|
210
|
+
<li class="change-item <%= change[:type] || change['type'] %>">
|
|
211
|
+
<div class="change-type"><%= change[:type] || change['type'] %></div>
|
|
212
|
+
<div class="change-detail">
|
|
213
|
+
<% if change[:model] || change['model'] %>
|
|
214
|
+
<strong>Model:</strong> <%= change[:model] || change['model'] %>
|
|
215
|
+
<% end %>
|
|
216
|
+
<% if change[:column] || change['column'] %>
|
|
217
|
+
→ <strong>Column:</strong> <code><%= change[:column] || change['column'] %></code>
|
|
218
|
+
<% end %>
|
|
219
|
+
<br>
|
|
220
|
+
<% if change[:message] || change['message'] %>
|
|
221
|
+
<%= change[:message] || change['message'] %>
|
|
222
|
+
<% end %>
|
|
223
|
+
<% if change[:old_value] || change['old_value'] || change[:new_value] || change['new_value'] %>
|
|
224
|
+
<br>
|
|
225
|
+
<strong>From:</strong> <code><%= change[:old_value] || change['old_value'] || "(none)" %></code> →
|
|
226
|
+
<strong>To:</strong> <code><%= change[:new_value] || change['new_value'] || "(none)" %></code>
|
|
227
|
+
<% end %>
|
|
228
|
+
</div>
|
|
229
|
+
</li>
|
|
230
|
+
<% end %>
|
|
231
|
+
</ul>
|
|
232
|
+
<% else %>
|
|
233
|
+
<p class="no-changes">No pending changes. Schema is up to date.</p>
|
|
234
|
+
<% end %>
|
|
235
|
+
|
|
236
|
+
<% if @history.any? %>
|
|
237
|
+
<h2>Version History</h2>
|
|
238
|
+
<p class="section-desc">
|
|
239
|
+
All committed schema versions. Click on a version to view its complete details and compare with previous versions.
|
|
240
|
+
</p>
|
|
241
|
+
|
|
242
|
+
<div class="config-card">
|
|
243
|
+
<div class="config-body" style="padding: 0;">
|
|
244
|
+
<table class="config-table" style="margin: 0;">
|
|
245
|
+
<thead>
|
|
246
|
+
<tr>
|
|
247
|
+
<th style="width: 80px;">Version</th>
|
|
248
|
+
<th style="width: 180px;">Created At</th>
|
|
249
|
+
<th style="width: 100px;">Lyra</th>
|
|
250
|
+
<th>Fingerprint</th>
|
|
251
|
+
<th style="width: 100px;">Models</th>
|
|
252
|
+
<th style="width: 80px;"></th>
|
|
253
|
+
</tr>
|
|
254
|
+
</thead>
|
|
255
|
+
<tbody>
|
|
256
|
+
<% @history.each do |entry| %>
|
|
257
|
+
<tr>
|
|
258
|
+
<td>
|
|
259
|
+
<span class="badge <%= entry[:version] == @version ? 'badge-success' : 'badge-secondary' %>">
|
|
260
|
+
v<%= entry[:version] %>
|
|
261
|
+
</span>
|
|
262
|
+
<% if entry[:version] == @version %>
|
|
263
|
+
<span style="font-size: 10px; color: #28a745; margin-left: 4px;">current</span>
|
|
264
|
+
<% end %>
|
|
265
|
+
</td>
|
|
266
|
+
<td style="font-size: 13px; color: #6c757d;">
|
|
267
|
+
<%= entry[:created_at].is_a?(String) ? entry[:created_at] : entry[:created_at]&.strftime("%Y-%m-%d %H:%M") %>
|
|
268
|
+
</td>
|
|
269
|
+
<td><code style="font-size: 12px;"><%= entry[:lyra_version] %></code></td>
|
|
270
|
+
<td><code style="font-size: 11px; color: #6c757d;"><%= entry[:fingerprint]&.first(20) %>...</code></td>
|
|
271
|
+
<td style="text-align: center;"><%= entry[:models_count] || "-" %></td>
|
|
272
|
+
<td>
|
|
273
|
+
<%= link_to "View", schema_version_path(version: entry[:version]), style: "font-size: 12px; color: #007bff; text-decoration: none;" %>
|
|
274
|
+
</td>
|
|
275
|
+
</tr>
|
|
276
|
+
<% end %>
|
|
277
|
+
</tbody>
|
|
278
|
+
</table>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
<% end %>
|
|
282
|
+
<% end %>
|
|
283
|
+
</div>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.lyra-schema-history { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
3
|
+
.lyra-schema-history h1 { color: #1a1a2e; border-bottom: 2px solid #4a4e69; padding-bottom: 10px; }
|
|
4
|
+
.lyra-schema-history .back-link { display: inline-block; margin-bottom: 20px; color: #4a4e69; text-decoration: none; }
|
|
5
|
+
.lyra-schema-history .back-link:hover { text-decoration: underline; }
|
|
6
|
+
.lyra-schema-history .section-desc { color: #6c757d; font-size: 13px; margin-bottom: 15px; }
|
|
7
|
+
.lyra-schema-history .config-card { background: #fff; border: 1px solid #dee2e6; border-radius: 8px; margin-bottom: 20px; overflow: hidden; }
|
|
8
|
+
.lyra-schema-history .config-table { width: 100%; border-collapse: collapse; }
|
|
9
|
+
.lyra-schema-history .config-table th { text-align: left; padding: 12px 15px; background: #f8f9fa; border-bottom: 1px solid #dee2e6; font-weight: 600; color: #495057; }
|
|
10
|
+
.lyra-schema-history .config-table td { padding: 12px 15px; border-bottom: 1px solid #eee; }
|
|
11
|
+
.lyra-schema-history .config-table tr:last-child td { border-bottom: none; }
|
|
12
|
+
.lyra-schema-history .config-table tr:hover { background: #f8f9fa; }
|
|
13
|
+
.lyra-schema-history .badge { display: inline-block; padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: 600; }
|
|
14
|
+
.lyra-schema-history .badge-success { background: #d4edda; color: #155724; }
|
|
15
|
+
.lyra-schema-history .badge-secondary { background: #e2e3e5; color: #383d41; }
|
|
16
|
+
.lyra-schema-history code { background: #f8f9fa; padding: 2px 6px; border-radius: 3px; font-size: 12px; }
|
|
17
|
+
.lyra-schema-history .view-btn { padding: 6px 12px; background: #4a4e69; color: #fff; text-decoration: none; border-radius: 4px; font-size: 12px; }
|
|
18
|
+
.lyra-schema-history .view-btn:hover { background: #22223b; }
|
|
19
|
+
.lyra-schema-history .empty-state { text-align: center; padding: 40px; color: #6c757d; }
|
|
20
|
+
</style>
|
|
21
|
+
|
|
22
|
+
<div class="lyra-schema-history">
|
|
23
|
+
<%= link_to "← Back to Schema Registry".html_safe, schema_path, class: "back-link" %>
|
|
24
|
+
|
|
25
|
+
<h1>Schema Version History</h1>
|
|
26
|
+
<p class="section-desc">
|
|
27
|
+
Complete history of all committed schema versions. Each version is an immutable snapshot of your
|
|
28
|
+
model configurations and event structures at a point in time.
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
<% if @history.any? %>
|
|
32
|
+
<div class="config-card">
|
|
33
|
+
<table class="config-table">
|
|
34
|
+
<thead>
|
|
35
|
+
<tr>
|
|
36
|
+
<th style="width: 100px;">Version</th>
|
|
37
|
+
<th style="width: 200px;">Created At</th>
|
|
38
|
+
<th style="width: 100px;">Lyra</th>
|
|
39
|
+
<th>Fingerprint</th>
|
|
40
|
+
<th style="width: 80px;">Models</th>
|
|
41
|
+
<th style="width: 100px;"></th>
|
|
42
|
+
</tr>
|
|
43
|
+
</thead>
|
|
44
|
+
<tbody>
|
|
45
|
+
<% @history.each do |entry| %>
|
|
46
|
+
<tr>
|
|
47
|
+
<td>
|
|
48
|
+
<span class="badge <%= entry[:version] == @current_version ? 'badge-success' : 'badge-secondary' %>">
|
|
49
|
+
v<%= entry[:version] %>
|
|
50
|
+
</span>
|
|
51
|
+
<% if entry[:version] == @current_version %>
|
|
52
|
+
<br><span style="font-size: 10px; color: #28a745;">current</span>
|
|
53
|
+
<% end %>
|
|
54
|
+
</td>
|
|
55
|
+
<td style="font-size: 13px;">
|
|
56
|
+
<%= entry[:created_at].is_a?(String) ? entry[:created_at] : entry[:created_at]&.strftime("%Y-%m-%d %H:%M:%S") %>
|
|
57
|
+
</td>
|
|
58
|
+
<td><code><%= entry[:lyra_version] %></code></td>
|
|
59
|
+
<td><code style="font-size: 11px; color: #6c757d;"><%= entry[:fingerprint] %></code></td>
|
|
60
|
+
<td style="text-align: center;"><%= entry[:models_count] || "-" %></td>
|
|
61
|
+
<td>
|
|
62
|
+
<%= link_to "View Details", schema_version_path(version: entry[:version]), class: "view-btn" %>
|
|
63
|
+
</td>
|
|
64
|
+
</tr>
|
|
65
|
+
<% end %>
|
|
66
|
+
</tbody>
|
|
67
|
+
</table>
|
|
68
|
+
</div>
|
|
69
|
+
<% else %>
|
|
70
|
+
<div class="config-card">
|
|
71
|
+
<div class="empty-state">
|
|
72
|
+
<h3>No Schema Versions</h3>
|
|
73
|
+
<p>No schema versions have been committed yet.</p>
|
|
74
|
+
<p>Run <code>rake lyra:schema:commit</code> to create your first schema version.</p>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<% end %>
|
|
78
|
+
</div>
|