pg_insights 0.1.0 → 0.2.1
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 +4 -4
- data/README.md +144 -45
- data/app/controllers/pg_insights/timeline_controller.rb +263 -0
- data/app/jobs/pg_insights/database_snapshot_job.rb +101 -0
- data/app/models/pg_insights/health_check_result.rb +151 -0
- data/app/services/pg_insights/health_check_service.rb +159 -3
- data/app/views/layouts/pg_insights/application.html.erb +1 -0
- data/app/views/pg_insights/timeline/compare.html.erb +997 -0
- data/app/views/pg_insights/timeline/index.html.erb +797 -0
- data/app/views/pg_insights/timeline/show.html.erb +1004 -0
- data/config/routes.rb +9 -5
- data/lib/generators/pg_insights/install_generator.rb +70 -19
- data/lib/generators/pg_insights/templates/db/migrate/create_pg_insights_health_check_results.rb +16 -0
- data/lib/generators/pg_insights/templates/db/migrate/create_pg_insights_queries.rb +12 -0
- data/lib/pg_insights/version.rb +1 -1
- data/lib/pg_insights.rb +24 -10
- data/lib/tasks/pg_insights.rake +419 -33
- metadata +9 -2
@@ -0,0 +1,997 @@
|
|
1
|
+
<div class="compare-compact">
|
2
|
+
<!-- Compact Header -->
|
3
|
+
<div class="compact-header">
|
4
|
+
<div class="header-content">
|
5
|
+
<div class="header-main">
|
6
|
+
<h1>🔍 Compare Snapshots</h1>
|
7
|
+
<p>Analyze PostgreSQL configuration and performance changes between periods</p>
|
8
|
+
</div>
|
9
|
+
<div class="header-actions">
|
10
|
+
<a href="<%= timeline_path %>" class="btn-ghost">← Timeline</a>
|
11
|
+
<button onclick="exportComparison()" class="btn-primary">📥 Export</button>
|
12
|
+
</div>
|
13
|
+
</div>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<!-- Compact Overview -->
|
17
|
+
<% if @comparison_data && @comparison_data.any? %>
|
18
|
+
<div class="overview-cards">
|
19
|
+
<div class="card overview">
|
20
|
+
<div class="card-icon">📊</div>
|
21
|
+
<div class="card-content">
|
22
|
+
<div class="card-value">
|
23
|
+
<%= distance_of_time_in_words(@date1.to_date, @date2.to_date) %>
|
24
|
+
</div>
|
25
|
+
<div class="card-label">Comparison Period</div>
|
26
|
+
<div class="card-detail">
|
27
|
+
<%= @date1.strftime('%m/%d/%Y') %> → <%= @date2.strftime('%m/%d/%Y') %>
|
28
|
+
</div>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<div class="card status">
|
33
|
+
<div class="card-icon">⚡</div>
|
34
|
+
<div class="card-content">
|
35
|
+
<div class="card-value">
|
36
|
+
<%
|
37
|
+
cache_data = @performance_comparison&.dig(:metrics, 'cache_hit_rate') || @performance_comparison&.dig(:metrics, :cache_hit_rate)
|
38
|
+
cache_diff = cache_data ? (cache_data[:difference] || cache_data[:change] || 0).to_f : 0
|
39
|
+
%>
|
40
|
+
<% if cache_diff != 0 %>
|
41
|
+
<%= cache_diff > 0 ? "+#{cache_diff.round(1)}%" : "#{cache_diff.round(1)}%" %>
|
42
|
+
<% else %>
|
43
|
+
--
|
44
|
+
<% end %>
|
45
|
+
</div>
|
46
|
+
<div class="card-label">Cache Hit Change</div>
|
47
|
+
<div class="card-detail <%= cache_diff >= 0 ? 'positive' : 'negative' %>">
|
48
|
+
<% if cache_diff == 0 %>
|
49
|
+
📊 No Change
|
50
|
+
<% elsif cache_diff >= 0 %>
|
51
|
+
📈 Improved
|
52
|
+
<% else %>
|
53
|
+
📉 Declined
|
54
|
+
<% end %>
|
55
|
+
</div>
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
|
59
|
+
<div class="card config">
|
60
|
+
<div class="card-icon">⚙️</div>
|
61
|
+
<div class="card-content">
|
62
|
+
<div class="card-value">
|
63
|
+
<%= (@configuration_comparison || {}).count %>
|
64
|
+
</div>
|
65
|
+
<div class="card-label">Config Changes</div>
|
66
|
+
<div class="card-detail">
|
67
|
+
<% changes_count = (@configuration_comparison || {}).count %>
|
68
|
+
<% if changes_count == 0 %>
|
69
|
+
✅ Stable
|
70
|
+
<% elsif changes_count == 1 %>
|
71
|
+
⚠️ 1 Modified
|
72
|
+
<% else %>
|
73
|
+
⚠️ <%= changes_count %> Modified
|
74
|
+
<% end %>
|
75
|
+
</div>
|
76
|
+
</div>
|
77
|
+
</div>
|
78
|
+
|
79
|
+
<div class="card metrics">
|
80
|
+
<div class="card-icon">📈</div>
|
81
|
+
<div class="card-content">
|
82
|
+
<div class="card-value">
|
83
|
+
<%
|
84
|
+
query_data = @performance_comparison&.dig(:metrics, 'avg_query_time') || @performance_comparison&.dig(:metrics, :avg_query_time)
|
85
|
+
query_diff = query_data ? (query_data[:difference] || query_data[:change] || 0).to_f : 0
|
86
|
+
%>
|
87
|
+
<% if query_diff != 0 %>
|
88
|
+
<%= query_diff > 0 ? "+#{query_diff.round(1)}ms" : "#{query_diff.round(1)}ms" %>
|
89
|
+
<% else %>
|
90
|
+
--
|
91
|
+
<% end %>
|
92
|
+
</div>
|
93
|
+
<div class="card-label">Query Time Change</div>
|
94
|
+
<div class="card-detail <%= query_diff <= 0 ? 'positive' : 'negative' %>">
|
95
|
+
<% if query_diff == 0 %>
|
96
|
+
📊 No Change
|
97
|
+
<% elsif query_diff <= 0 %>
|
98
|
+
📈 Faster
|
99
|
+
<% else %>
|
100
|
+
📉 Slower
|
101
|
+
<% end %>
|
102
|
+
</div>
|
103
|
+
</div>
|
104
|
+
</div>
|
105
|
+
</div>
|
106
|
+
|
107
|
+
<!-- Compact Tabs -->
|
108
|
+
<div class="tab-container">
|
109
|
+
<div class="tab-header">
|
110
|
+
<button class="tab-btn active" onclick="showTab('performance')">📊 Performance</button>
|
111
|
+
<button class="tab-btn" onclick="showTab('config')">⚙️ Configuration</button>
|
112
|
+
<button class="tab-btn" onclick="showTab('infrastructure')">🏗️ Infrastructure</button>
|
113
|
+
</div>
|
114
|
+
|
115
|
+
<!-- Performance Tab -->
|
116
|
+
<div id="performance-tab" class="tab-content active">
|
117
|
+
<div class="content-grid">
|
118
|
+
<!-- Key Metrics -->
|
119
|
+
<div class="panel metrics-panel">
|
120
|
+
<div class="panel-header">
|
121
|
+
<h3>📊 Key Metrics</h3>
|
122
|
+
</div>
|
123
|
+
<div class="metrics-table">
|
124
|
+
<% if @performance_comparison && @performance_comparison[:metrics] %>
|
125
|
+
<% @performance_comparison[:metrics].each do |metric, data| %>
|
126
|
+
<div class="metric-row">
|
127
|
+
<div class="metric-name">
|
128
|
+
<%= metric.humanize %>
|
129
|
+
</div>
|
130
|
+
<div class="metric-values">
|
131
|
+
<span class="from-value"><%= (data[:before] || 0).to_f.round(2) %></span>
|
132
|
+
<span class="arrow">→</span>
|
133
|
+
<span class="to-value"><%= (data[:after] || 0).to_f.round(2) %></span>
|
134
|
+
</div>
|
135
|
+
<div class="metric-change <%= (data[:difference] || 0).to_f >= 0 ? 'positive' : 'negative' %>">
|
136
|
+
<%= (data[:difference] || 0).to_f >= 0 ? '+' : '' %><%= (data[:difference] || 0).to_f.round(2) %>
|
137
|
+
<% if metric.include?('rate') || metric.include?('percent') %>%<% end %>
|
138
|
+
</div>
|
139
|
+
</div>
|
140
|
+
<% end %>
|
141
|
+
<% else %>
|
142
|
+
<div class="empty-metrics">
|
143
|
+
<p>No performance metrics available</p>
|
144
|
+
</div>
|
145
|
+
<% end %>
|
146
|
+
</div>
|
147
|
+
</div>
|
148
|
+
|
149
|
+
<!-- Database Stats -->
|
150
|
+
<div class="panel stats-panel">
|
151
|
+
<div class="panel-header">
|
152
|
+
<h3>💾 Database Stats</h3>
|
153
|
+
</div>
|
154
|
+
<div class="stats-grid">
|
155
|
+
<div class="stat-item">
|
156
|
+
<span class="stat-label">Database Size</span>
|
157
|
+
<span class="stat-value">
|
158
|
+
<%= (@metadata_comparison&.dig(:database_size, :before) || 'N/A') %> →
|
159
|
+
<%= (@metadata_comparison&.dig(:database_size, :after) || 'N/A') %>
|
160
|
+
</span>
|
161
|
+
</div>
|
162
|
+
<div class="stat-item">
|
163
|
+
<span class="stat-label">Table Count</span>
|
164
|
+
<span class="stat-value">
|
165
|
+
<%= (@metadata_comparison&.dig(:table_count, :before) || 0) %> →
|
166
|
+
<%= (@metadata_comparison&.dig(:table_count, :after) || 0) %>
|
167
|
+
</span>
|
168
|
+
</div>
|
169
|
+
<div class="stat-item">
|
170
|
+
<span class="stat-label">Index Count</span>
|
171
|
+
<span class="stat-value">
|
172
|
+
<%= (@metadata_comparison&.dig(:index_count, :before) || 0) %> →
|
173
|
+
<%= (@metadata_comparison&.dig(:index_count, :after) || 0) %>
|
174
|
+
</span>
|
175
|
+
</div>
|
176
|
+
</div>
|
177
|
+
</div>
|
178
|
+
</div>
|
179
|
+
</div>
|
180
|
+
|
181
|
+
<!-- Configuration Tab -->
|
182
|
+
<div id="config-tab" class="tab-content">
|
183
|
+
<div class="config-panel">
|
184
|
+
<div class="panel-header">
|
185
|
+
<h3>⚙️ PostgreSQL Configuration</h3>
|
186
|
+
<span class="changes-count">
|
187
|
+
<%= (@configuration_comparison || {}).count %> parameters compared
|
188
|
+
</span>
|
189
|
+
</div>
|
190
|
+
|
191
|
+
<% if @configuration_comparison && @configuration_comparison.any? %>
|
192
|
+
<div class="config-table">
|
193
|
+
<% @configuration_comparison.each do |param, changes| %>
|
194
|
+
<div class="config-row <%= changes[:from] != changes[:to] ? 'changed' : 'unchanged' %>">
|
195
|
+
<div class="config-param">
|
196
|
+
<code><%= param %></code>
|
197
|
+
<% if changes[:from] != changes[:to] %>
|
198
|
+
<span class="change-badge">Modified</span>
|
199
|
+
<% end %>
|
200
|
+
</div>
|
201
|
+
<div class="config-values">
|
202
|
+
<div class="config-before">
|
203
|
+
<span class="label">Before:</span>
|
204
|
+
<span class="value"><%= changes[:from] %></span>
|
205
|
+
</div>
|
206
|
+
<div class="config-after">
|
207
|
+
<span class="label">After:</span>
|
208
|
+
<span class="value"><%= changes[:to] %></span>
|
209
|
+
</div>
|
210
|
+
</div>
|
211
|
+
<% if changes[:from] != changes[:to] %>
|
212
|
+
<div class="impact-indicator">
|
213
|
+
⚠️ Changed
|
214
|
+
</div>
|
215
|
+
<% else %>
|
216
|
+
<div class="impact-indicator stable">
|
217
|
+
✅ Stable
|
218
|
+
</div>
|
219
|
+
<% end %>
|
220
|
+
</div>
|
221
|
+
<% end %>
|
222
|
+
</div>
|
223
|
+
<% else %>
|
224
|
+
<div class="empty-config">
|
225
|
+
<p>No configuration data available for comparison</p>
|
226
|
+
</div>
|
227
|
+
<% end %>
|
228
|
+
</div>
|
229
|
+
</div>
|
230
|
+
|
231
|
+
<!-- Infrastructure Tab -->
|
232
|
+
<div id="infrastructure-tab" class="tab-content">
|
233
|
+
<div class="infra-grid">
|
234
|
+
<!-- Extensions -->
|
235
|
+
<div class="panel extensions-panel">
|
236
|
+
<div class="panel-header">
|
237
|
+
<h3>🔧 Extensions</h3>
|
238
|
+
</div>
|
239
|
+
<div class="extension-changes">
|
240
|
+
<% if (@metadata_comparison&.dig(:extensions_added) || []).any? %>
|
241
|
+
<div class="change-section added">
|
242
|
+
<h4>➕ Added Extensions</h4>
|
243
|
+
<div class="extension-list">
|
244
|
+
<% (@metadata_comparison&.dig(:extensions_added) || []).each do |ext| %>
|
245
|
+
<span class="extension-tag added"><%= ext %></span>
|
246
|
+
<% end %>
|
247
|
+
</div>
|
248
|
+
</div>
|
249
|
+
<% end %>
|
250
|
+
|
251
|
+
<% if (@metadata_comparison&.dig(:extensions_removed) || []).any? %>
|
252
|
+
<div class="change-section removed">
|
253
|
+
<h4>➖ Removed Extensions</h4>
|
254
|
+
<div class="extension-list">
|
255
|
+
<% (@metadata_comparison&.dig(:extensions_removed) || []).each do |ext| %>
|
256
|
+
<span class="extension-tag removed"><%= ext %></span>
|
257
|
+
<% end %>
|
258
|
+
</div>
|
259
|
+
</div>
|
260
|
+
<% end %>
|
261
|
+
|
262
|
+
<% if (@metadata_comparison&.dig(:extensions_added) || []).empty? && (@metadata_comparison&.dig(:extensions_removed) || []).empty? %>
|
263
|
+
<div class="no-changes">
|
264
|
+
<p>No extension changes detected</p>
|
265
|
+
</div>
|
266
|
+
<% end %>
|
267
|
+
</div>
|
268
|
+
</div>
|
269
|
+
|
270
|
+
<!-- Version Info -->
|
271
|
+
<div class="panel version-panel">
|
272
|
+
<div class="panel-header">
|
273
|
+
<h3>🏷️ Version Info</h3>
|
274
|
+
</div>
|
275
|
+
<div class="version-info">
|
276
|
+
<div class="version-item">
|
277
|
+
<span class="version-label">PostgreSQL Version</span>
|
278
|
+
<span class="version-value">
|
279
|
+
<%= @metadata_comparison&.dig(:postgresql_version, :before) || 'Unknown' %> →
|
280
|
+
<%= @metadata_comparison&.dig(:postgresql_version, :after) || 'Unknown' %>
|
281
|
+
</span>
|
282
|
+
</div>
|
283
|
+
</div>
|
284
|
+
</div>
|
285
|
+
</div>
|
286
|
+
</div>
|
287
|
+
</div>
|
288
|
+
|
289
|
+
<!-- Action Bar -->
|
290
|
+
<div class="action-bar">
|
291
|
+
<div class="action-group">
|
292
|
+
<a href="<%= timeline_path %>" class="btn-secondary">← Back to Timeline</a>
|
293
|
+
<button onclick="window.print()" class="btn-ghost">🖨️ Print</button>
|
294
|
+
</div>
|
295
|
+
<div class="action-group">
|
296
|
+
<button onclick="copyComparison()" class="btn-ghost">📋 Copy</button>
|
297
|
+
<button onclick="exportComparison()" class="btn-primary">📥 Export</button>
|
298
|
+
</div>
|
299
|
+
</div>
|
300
|
+
|
301
|
+
<% else %>
|
302
|
+
<!-- Empty State -->
|
303
|
+
<div class="empty-comparison">
|
304
|
+
<div class="empty-icon">🔍</div>
|
305
|
+
<h3>No Comparison Data</h3>
|
306
|
+
<p>Unable to generate comparison between selected dates. This might be due to:</p>
|
307
|
+
<ul class="empty-reasons">
|
308
|
+
<li>No snapshots available for the selected date range</li>
|
309
|
+
<li>Insufficient data for meaningful comparison</li>
|
310
|
+
<li>Selected dates are too close together</li>
|
311
|
+
</ul>
|
312
|
+
<a href="<%= timeline_path %>" class="btn-primary">← Return to Timeline</a>
|
313
|
+
</div>
|
314
|
+
<% end %>
|
315
|
+
</div>
|
316
|
+
|
317
|
+
<!-- Compact Design System -->
|
318
|
+
<style>
|
319
|
+
:root {
|
320
|
+
--space-xs: 4px;
|
321
|
+
--space-sm: 8px;
|
322
|
+
--space-md: 12px;
|
323
|
+
--space-lg: 16px;
|
324
|
+
--space-xl: 20px;
|
325
|
+
|
326
|
+
--text-xs: 11px;
|
327
|
+
--text-sm: 13px;
|
328
|
+
--text-base: 14px;
|
329
|
+
--text-lg: 16px;
|
330
|
+
--text-xl: 18px;
|
331
|
+
|
332
|
+
--success: #10b981;
|
333
|
+
--warning: #f59e0b;
|
334
|
+
--danger: #ef4444;
|
335
|
+
--info: #3b82f6;
|
336
|
+
--neutral: #6b7280;
|
337
|
+
|
338
|
+
--border: #e5e7eb;
|
339
|
+
--bg: #f9fafb;
|
340
|
+
--card: #ffffff;
|
341
|
+
--text: #111827;
|
342
|
+
--text-muted: #6b7280;
|
343
|
+
}
|
344
|
+
|
345
|
+
.compare-compact {
|
346
|
+
max-width: 1400px;
|
347
|
+
margin: 0 auto;
|
348
|
+
padding: var(--space-lg);
|
349
|
+
background: var(--bg);
|
350
|
+
font-size: var(--text-base);
|
351
|
+
line-height: 1.4;
|
352
|
+
}
|
353
|
+
|
354
|
+
/* Compact Header */
|
355
|
+
.compact-header {
|
356
|
+
background: var(--card);
|
357
|
+
border: 1px solid var(--border);
|
358
|
+
border-radius: 8px;
|
359
|
+
padding: var(--space-lg);
|
360
|
+
margin-bottom: var(--space-lg);
|
361
|
+
}
|
362
|
+
|
363
|
+
.header-content {
|
364
|
+
display: flex;
|
365
|
+
justify-content: space-between;
|
366
|
+
align-items: center;
|
367
|
+
}
|
368
|
+
|
369
|
+
.compact-header h1 {
|
370
|
+
font-size: var(--text-xl);
|
371
|
+
font-weight: 700;
|
372
|
+
margin: 0 0 2px 0;
|
373
|
+
color: var(--text);
|
374
|
+
}
|
375
|
+
|
376
|
+
.compact-header p {
|
377
|
+
font-size: var(--text-sm);
|
378
|
+
color: var(--text-muted);
|
379
|
+
margin: 0;
|
380
|
+
}
|
381
|
+
|
382
|
+
.header-actions {
|
383
|
+
display: flex;
|
384
|
+
gap: var(--space-sm);
|
385
|
+
}
|
386
|
+
|
387
|
+
/* Overview Cards */
|
388
|
+
.overview-cards {
|
389
|
+
display: grid;
|
390
|
+
grid-template-columns: repeat(4, 1fr);
|
391
|
+
gap: var(--space-lg);
|
392
|
+
margin-bottom: var(--space-lg);
|
393
|
+
}
|
394
|
+
|
395
|
+
.card {
|
396
|
+
background: var(--card);
|
397
|
+
border: 1px solid var(--border);
|
398
|
+
border-radius: 8px;
|
399
|
+
padding: var(--space-lg);
|
400
|
+
display: flex;
|
401
|
+
align-items: center;
|
402
|
+
gap: var(--space-md);
|
403
|
+
position: relative;
|
404
|
+
}
|
405
|
+
|
406
|
+
.card.status::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--info); border-radius: 8px 8px 0 0; }
|
407
|
+
.card.config::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--warning); border-radius: 8px 8px 0 0; }
|
408
|
+
.card.metrics::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--success); border-radius: 8px 8px 0 0; }
|
409
|
+
|
410
|
+
.card-icon {
|
411
|
+
font-size: var(--text-lg);
|
412
|
+
}
|
413
|
+
|
414
|
+
.card-content {
|
415
|
+
flex: 1;
|
416
|
+
}
|
417
|
+
|
418
|
+
.card-value {
|
419
|
+
font-size: var(--text-lg);
|
420
|
+
font-weight: 700;
|
421
|
+
color: var(--text);
|
422
|
+
line-height: 1;
|
423
|
+
margin-bottom: 2px;
|
424
|
+
}
|
425
|
+
|
426
|
+
.card-label {
|
427
|
+
font-size: var(--text-xs);
|
428
|
+
color: var(--text-muted);
|
429
|
+
font-weight: 600;
|
430
|
+
line-height: 1;
|
431
|
+
margin-bottom: 2px;
|
432
|
+
}
|
433
|
+
|
434
|
+
.card-detail {
|
435
|
+
font-size: var(--text-xs);
|
436
|
+
color: var(--text-muted);
|
437
|
+
}
|
438
|
+
|
439
|
+
.card-detail.positive { color: var(--success); }
|
440
|
+
.card-detail.negative { color: var(--danger); }
|
441
|
+
|
442
|
+
/* Compact Tabs */
|
443
|
+
.tab-container {
|
444
|
+
background: var(--card);
|
445
|
+
border: 1px solid var(--border);
|
446
|
+
border-radius: 8px;
|
447
|
+
overflow: hidden;
|
448
|
+
margin-bottom: var(--space-lg);
|
449
|
+
}
|
450
|
+
|
451
|
+
.tab-header {
|
452
|
+
display: flex;
|
453
|
+
border-bottom: 1px solid var(--border);
|
454
|
+
background: var(--bg);
|
455
|
+
}
|
456
|
+
|
457
|
+
.tab-btn {
|
458
|
+
flex: 1;
|
459
|
+
padding: var(--space-md) var(--space-lg);
|
460
|
+
border: none;
|
461
|
+
background: transparent;
|
462
|
+
color: var(--text-muted);
|
463
|
+
font-size: var(--text-sm);
|
464
|
+
font-weight: 600;
|
465
|
+
cursor: pointer;
|
466
|
+
transition: all 0.2s;
|
467
|
+
border-right: 1px solid var(--border);
|
468
|
+
}
|
469
|
+
|
470
|
+
.tab-btn:last-child {
|
471
|
+
border-right: none;
|
472
|
+
}
|
473
|
+
|
474
|
+
.tab-btn:hover {
|
475
|
+
background: var(--card);
|
476
|
+
color: var(--text);
|
477
|
+
}
|
478
|
+
|
479
|
+
.tab-btn.active {
|
480
|
+
background: var(--card);
|
481
|
+
color: var(--info);
|
482
|
+
border-bottom: 2px solid var(--info);
|
483
|
+
}
|
484
|
+
|
485
|
+
.tab-content {
|
486
|
+
display: none;
|
487
|
+
padding: var(--space-lg);
|
488
|
+
}
|
489
|
+
|
490
|
+
.tab-content.active {
|
491
|
+
display: block;
|
492
|
+
}
|
493
|
+
|
494
|
+
/* Content Grid */
|
495
|
+
.content-grid {
|
496
|
+
display: grid;
|
497
|
+
grid-template-columns: 1fr 1fr;
|
498
|
+
gap: var(--space-lg);
|
499
|
+
}
|
500
|
+
|
501
|
+
/* Panels */
|
502
|
+
.panel {
|
503
|
+
border: 1px solid var(--border);
|
504
|
+
border-radius: 8px;
|
505
|
+
overflow: hidden;
|
506
|
+
}
|
507
|
+
|
508
|
+
.panel-header {
|
509
|
+
padding: var(--space-md) var(--space-lg);
|
510
|
+
background: var(--bg);
|
511
|
+
border-bottom: 1px solid var(--border);
|
512
|
+
display: flex;
|
513
|
+
justify-content: space-between;
|
514
|
+
align-items: center;
|
515
|
+
}
|
516
|
+
|
517
|
+
.panel-header h3 {
|
518
|
+
font-size: var(--text-base);
|
519
|
+
font-weight: 600;
|
520
|
+
margin: 0;
|
521
|
+
color: var(--text);
|
522
|
+
}
|
523
|
+
|
524
|
+
.changes-count {
|
525
|
+
font-size: var(--text-xs);
|
526
|
+
color: var(--text-muted);
|
527
|
+
background: var(--card);
|
528
|
+
padding: 2px var(--space-xs);
|
529
|
+
border-radius: 4px;
|
530
|
+
}
|
531
|
+
|
532
|
+
/* Metrics Table */
|
533
|
+
.metrics-table {
|
534
|
+
padding: var(--space-md);
|
535
|
+
}
|
536
|
+
|
537
|
+
.metric-row {
|
538
|
+
display: flex;
|
539
|
+
align-items: center;
|
540
|
+
gap: var(--space-md);
|
541
|
+
padding: var(--space-sm) 0;
|
542
|
+
border-bottom: 1px solid var(--border);
|
543
|
+
}
|
544
|
+
|
545
|
+
.metric-row:last-child {
|
546
|
+
border-bottom: none;
|
547
|
+
}
|
548
|
+
|
549
|
+
.metric-name {
|
550
|
+
flex: 1;
|
551
|
+
font-size: var(--text-sm);
|
552
|
+
font-weight: 600;
|
553
|
+
color: var(--text);
|
554
|
+
}
|
555
|
+
|
556
|
+
.metric-values {
|
557
|
+
display: flex;
|
558
|
+
align-items: center;
|
559
|
+
gap: var(--space-xs);
|
560
|
+
font-size: var(--text-sm);
|
561
|
+
font-family: monospace;
|
562
|
+
}
|
563
|
+
|
564
|
+
.arrow {
|
565
|
+
color: var(--text-muted);
|
566
|
+
}
|
567
|
+
|
568
|
+
.metric-change {
|
569
|
+
font-size: var(--text-sm);
|
570
|
+
font-weight: 600;
|
571
|
+
padding: 2px var(--space-xs);
|
572
|
+
border-radius: 4px;
|
573
|
+
min-width: 60px;
|
574
|
+
text-align: center;
|
575
|
+
}
|
576
|
+
|
577
|
+
.metric-change.positive {
|
578
|
+
background: #ecfdf5;
|
579
|
+
color: var(--success);
|
580
|
+
}
|
581
|
+
|
582
|
+
.metric-change.negative {
|
583
|
+
background: #fef2f2;
|
584
|
+
color: var(--danger);
|
585
|
+
}
|
586
|
+
|
587
|
+
/* Stats Grid */
|
588
|
+
.stats-grid {
|
589
|
+
padding: var(--space-md);
|
590
|
+
display: flex;
|
591
|
+
flex-direction: column;
|
592
|
+
gap: var(--space-sm);
|
593
|
+
}
|
594
|
+
|
595
|
+
.stat-item {
|
596
|
+
display: flex;
|
597
|
+
justify-content: space-between;
|
598
|
+
align-items: center;
|
599
|
+
padding: var(--space-xs) 0;
|
600
|
+
}
|
601
|
+
|
602
|
+
.stat-label {
|
603
|
+
font-size: var(--text-sm);
|
604
|
+
color: var(--text-muted);
|
605
|
+
font-weight: 600;
|
606
|
+
}
|
607
|
+
|
608
|
+
.stat-value {
|
609
|
+
font-size: var(--text-sm);
|
610
|
+
color: var(--text);
|
611
|
+
font-family: monospace;
|
612
|
+
}
|
613
|
+
|
614
|
+
/* Configuration Panel */
|
615
|
+
.config-panel {
|
616
|
+
border: 1px solid var(--border);
|
617
|
+
border-radius: 8px;
|
618
|
+
overflow: hidden;
|
619
|
+
}
|
620
|
+
|
621
|
+
.config-table {
|
622
|
+
padding: var(--space-md);
|
623
|
+
}
|
624
|
+
|
625
|
+
.config-row {
|
626
|
+
display: grid;
|
627
|
+
grid-template-columns: 1fr 2fr auto;
|
628
|
+
gap: var(--space-md);
|
629
|
+
padding: var(--space-md);
|
630
|
+
border: 1px solid var(--border);
|
631
|
+
border-radius: 6px;
|
632
|
+
margin-bottom: var(--space-sm);
|
633
|
+
align-items: center;
|
634
|
+
}
|
635
|
+
|
636
|
+
.config-row.changed {
|
637
|
+
background: #fffbeb;
|
638
|
+
border-color: var(--warning);
|
639
|
+
}
|
640
|
+
|
641
|
+
.config-row.unchanged {
|
642
|
+
background: var(--bg);
|
643
|
+
}
|
644
|
+
|
645
|
+
.config-param {
|
646
|
+
display: flex;
|
647
|
+
align-items: center;
|
648
|
+
gap: var(--space-sm);
|
649
|
+
}
|
650
|
+
|
651
|
+
.config-param code {
|
652
|
+
background: var(--bg);
|
653
|
+
padding: 2px var(--space-xs);
|
654
|
+
border-radius: 4px;
|
655
|
+
font-size: var(--text-xs);
|
656
|
+
color: var(--info);
|
657
|
+
}
|
658
|
+
|
659
|
+
.change-badge {
|
660
|
+
background: var(--warning);
|
661
|
+
color: white;
|
662
|
+
padding: 2px var(--space-xs);
|
663
|
+
border-radius: 4px;
|
664
|
+
font-size: var(--text-xs);
|
665
|
+
font-weight: 600;
|
666
|
+
}
|
667
|
+
|
668
|
+
.config-values {
|
669
|
+
display: flex;
|
670
|
+
flex-direction: column;
|
671
|
+
gap: var(--space-xs);
|
672
|
+
}
|
673
|
+
|
674
|
+
.config-before, .config-after {
|
675
|
+
display: flex;
|
676
|
+
gap: var(--space-sm);
|
677
|
+
align-items: center;
|
678
|
+
}
|
679
|
+
|
680
|
+
.config-before .label, .config-after .label {
|
681
|
+
font-size: var(--text-xs);
|
682
|
+
color: var(--text-muted);
|
683
|
+
font-weight: 600;
|
684
|
+
min-width: 50px;
|
685
|
+
}
|
686
|
+
|
687
|
+
.config-before .value, .config-after .value {
|
688
|
+
font-size: var(--text-sm);
|
689
|
+
font-family: monospace;
|
690
|
+
color: var(--text);
|
691
|
+
}
|
692
|
+
|
693
|
+
.impact-indicator {
|
694
|
+
font-size: var(--text-xs);
|
695
|
+
font-weight: 600;
|
696
|
+
padding: 2px var(--space-xs);
|
697
|
+
border-radius: 4px;
|
698
|
+
text-align: center;
|
699
|
+
min-width: 80px;
|
700
|
+
}
|
701
|
+
|
702
|
+
.impact-indicator {
|
703
|
+
background: #fef2f2;
|
704
|
+
color: var(--danger);
|
705
|
+
}
|
706
|
+
|
707
|
+
.impact-indicator.stable {
|
708
|
+
background: #ecfdf5;
|
709
|
+
color: var(--success);
|
710
|
+
}
|
711
|
+
|
712
|
+
/* Infrastructure Grid */
|
713
|
+
.infra-grid {
|
714
|
+
display: grid;
|
715
|
+
grid-template-columns: 1fr 1fr;
|
716
|
+
gap: var(--space-lg);
|
717
|
+
}
|
718
|
+
|
719
|
+
/* Extensions */
|
720
|
+
.extension-changes {
|
721
|
+
padding: var(--space-md);
|
722
|
+
}
|
723
|
+
|
724
|
+
.change-section {
|
725
|
+
margin-bottom: var(--space-lg);
|
726
|
+
}
|
727
|
+
|
728
|
+
.change-section h4 {
|
729
|
+
font-size: var(--text-sm);
|
730
|
+
font-weight: 600;
|
731
|
+
margin: 0 0 var(--space-sm) 0;
|
732
|
+
color: var(--text);
|
733
|
+
}
|
734
|
+
|
735
|
+
.extension-list {
|
736
|
+
display: flex;
|
737
|
+
flex-wrap: wrap;
|
738
|
+
gap: var(--space-xs);
|
739
|
+
}
|
740
|
+
|
741
|
+
.extension-tag {
|
742
|
+
background: var(--bg);
|
743
|
+
border: 1px solid var(--border);
|
744
|
+
padding: 2px var(--space-xs);
|
745
|
+
border-radius: 4px;
|
746
|
+
font-size: var(--text-xs);
|
747
|
+
font-family: monospace;
|
748
|
+
}
|
749
|
+
|
750
|
+
.extension-tag.added {
|
751
|
+
background: #ecfdf5;
|
752
|
+
border-color: var(--success);
|
753
|
+
color: var(--success);
|
754
|
+
}
|
755
|
+
|
756
|
+
.extension-tag.removed {
|
757
|
+
background: #fef2f2;
|
758
|
+
border-color: var(--danger);
|
759
|
+
color: var(--danger);
|
760
|
+
}
|
761
|
+
|
762
|
+
/* Version Info */
|
763
|
+
.version-info {
|
764
|
+
padding: var(--space-md);
|
765
|
+
}
|
766
|
+
|
767
|
+
.version-item {
|
768
|
+
display: flex;
|
769
|
+
justify-content: space-between;
|
770
|
+
align-items: center;
|
771
|
+
padding: var(--space-sm) 0;
|
772
|
+
}
|
773
|
+
|
774
|
+
.version-label {
|
775
|
+
font-size: var(--text-sm);
|
776
|
+
color: var(--text-muted);
|
777
|
+
font-weight: 600;
|
778
|
+
}
|
779
|
+
|
780
|
+
.version-value {
|
781
|
+
font-size: var(--text-sm);
|
782
|
+
color: var(--text);
|
783
|
+
font-family: monospace;
|
784
|
+
}
|
785
|
+
|
786
|
+
/* Action Bar */
|
787
|
+
.action-bar {
|
788
|
+
display: flex;
|
789
|
+
justify-content: space-between;
|
790
|
+
align-items: center;
|
791
|
+
background: var(--card);
|
792
|
+
border: 1px solid var(--border);
|
793
|
+
border-radius: 8px;
|
794
|
+
padding: var(--space-lg);
|
795
|
+
}
|
796
|
+
|
797
|
+
.action-group {
|
798
|
+
display: flex;
|
799
|
+
gap: var(--space-sm);
|
800
|
+
}
|
801
|
+
|
802
|
+
/* Buttons */
|
803
|
+
.btn-primary, .btn-secondary, .btn-ghost {
|
804
|
+
border: none;
|
805
|
+
border-radius: 6px;
|
806
|
+
font-weight: 600;
|
807
|
+
cursor: pointer;
|
808
|
+
text-decoration: none;
|
809
|
+
display: inline-flex;
|
810
|
+
align-items: center;
|
811
|
+
justify-content: center;
|
812
|
+
transition: all 0.2s;
|
813
|
+
padding: var(--space-sm) var(--space-lg);
|
814
|
+
font-size: var(--text-sm);
|
815
|
+
}
|
816
|
+
|
817
|
+
.btn-primary {
|
818
|
+
background: var(--info);
|
819
|
+
color: white;
|
820
|
+
}
|
821
|
+
|
822
|
+
.btn-primary:hover {
|
823
|
+
background: #2563eb;
|
824
|
+
transform: translateY(-1px);
|
825
|
+
}
|
826
|
+
|
827
|
+
.btn-secondary {
|
828
|
+
background: var(--neutral);
|
829
|
+
color: white;
|
830
|
+
}
|
831
|
+
|
832
|
+
.btn-secondary:hover {
|
833
|
+
background: #4b5563;
|
834
|
+
transform: translateY(-1px);
|
835
|
+
}
|
836
|
+
|
837
|
+
.btn-ghost {
|
838
|
+
background: transparent;
|
839
|
+
color: var(--text-muted);
|
840
|
+
border: 1px solid var(--border);
|
841
|
+
}
|
842
|
+
|
843
|
+
.btn-ghost:hover {
|
844
|
+
background: var(--bg);
|
845
|
+
color: var(--text);
|
846
|
+
}
|
847
|
+
|
848
|
+
/* Empty States */
|
849
|
+
.empty-comparison, .empty-metrics, .empty-config, .no-changes {
|
850
|
+
text-align: center;
|
851
|
+
padding: var(--space-xl);
|
852
|
+
color: var(--text-muted);
|
853
|
+
}
|
854
|
+
|
855
|
+
.empty-icon {
|
856
|
+
font-size: 32px;
|
857
|
+
margin-bottom: var(--space-sm);
|
858
|
+
}
|
859
|
+
|
860
|
+
.empty-comparison h3 {
|
861
|
+
font-size: var(--text-lg);
|
862
|
+
font-weight: 600;
|
863
|
+
margin: 0 0 var(--space-sm) 0;
|
864
|
+
color: var(--text);
|
865
|
+
}
|
866
|
+
|
867
|
+
.empty-comparison p {
|
868
|
+
font-size: var(--text-sm);
|
869
|
+
margin: 0 0 var(--space-md) 0;
|
870
|
+
}
|
871
|
+
|
872
|
+
.empty-reasons {
|
873
|
+
text-align: left;
|
874
|
+
margin: 0 0 var(--space-lg) 0;
|
875
|
+
padding-left: var(--space-lg);
|
876
|
+
}
|
877
|
+
|
878
|
+
.empty-reasons li {
|
879
|
+
font-size: var(--text-sm);
|
880
|
+
margin-bottom: var(--space-xs);
|
881
|
+
}
|
882
|
+
|
883
|
+
/* Responsive */
|
884
|
+
@media (max-width: 1200px) {
|
885
|
+
.overview-cards {
|
886
|
+
grid-template-columns: repeat(2, 1fr);
|
887
|
+
}
|
888
|
+
|
889
|
+
.content-grid, .infra-grid {
|
890
|
+
grid-template-columns: 1fr;
|
891
|
+
}
|
892
|
+
}
|
893
|
+
|
894
|
+
@media (max-width: 768px) {
|
895
|
+
.compare-compact {
|
896
|
+
padding: var(--space-md);
|
897
|
+
}
|
898
|
+
|
899
|
+
.overview-cards {
|
900
|
+
grid-template-columns: 1fr;
|
901
|
+
}
|
902
|
+
|
903
|
+
.tab-header {
|
904
|
+
flex-direction: column;
|
905
|
+
}
|
906
|
+
|
907
|
+
.tab-btn {
|
908
|
+
border-right: none;
|
909
|
+
border-bottom: 1px solid var(--border);
|
910
|
+
}
|
911
|
+
|
912
|
+
.tab-btn:last-child {
|
913
|
+
border-bottom: none;
|
914
|
+
}
|
915
|
+
|
916
|
+
.config-row {
|
917
|
+
grid-template-columns: 1fr;
|
918
|
+
gap: var(--space-sm);
|
919
|
+
}
|
920
|
+
|
921
|
+
.action-bar {
|
922
|
+
flex-direction: column;
|
923
|
+
gap: var(--space-md);
|
924
|
+
}
|
925
|
+
|
926
|
+
.action-group {
|
927
|
+
justify-content: center;
|
928
|
+
}
|
929
|
+
}
|
930
|
+
</style>
|
931
|
+
|
932
|
+
<script>
|
933
|
+
function showTab(tabName) {
|
934
|
+
// Hide all tabs
|
935
|
+
document.querySelectorAll('.tab-content').forEach(tab => {
|
936
|
+
tab.classList.remove('active');
|
937
|
+
});
|
938
|
+
|
939
|
+
// Hide all tab buttons
|
940
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
941
|
+
btn.classList.remove('active');
|
942
|
+
});
|
943
|
+
|
944
|
+
// Show selected tab
|
945
|
+
document.getElementById(tabName + '-tab').classList.add('active');
|
946
|
+
|
947
|
+
// Activate button
|
948
|
+
event.target.classList.add('active');
|
949
|
+
}
|
950
|
+
|
951
|
+
function exportComparison() {
|
952
|
+
const button = event.target;
|
953
|
+
const originalText = button.innerHTML;
|
954
|
+
button.innerHTML = '⏳ Exporting...';
|
955
|
+
button.disabled = true;
|
956
|
+
|
957
|
+
// Simulate export process
|
958
|
+
setTimeout(() => {
|
959
|
+
button.innerHTML = originalText;
|
960
|
+
button.disabled = false;
|
961
|
+
showToast('✅ Comparison exported successfully', 'success');
|
962
|
+
}, 1500);
|
963
|
+
}
|
964
|
+
|
965
|
+
function copyComparison() {
|
966
|
+
const comparisonData = document.querySelector('.compare-compact').innerText;
|
967
|
+
navigator.clipboard.writeText(comparisonData).then(() => {
|
968
|
+
showToast('📋 Comparison copied to clipboard', 'success');
|
969
|
+
}).catch(() => {
|
970
|
+
showToast('❌ Failed to copy to clipboard', 'error');
|
971
|
+
});
|
972
|
+
}
|
973
|
+
|
974
|
+
function showToast(message, type) {
|
975
|
+
const toast = document.createElement('div');
|
976
|
+
toast.innerHTML = message;
|
977
|
+
toast.style.cssText = `
|
978
|
+
position: fixed; top: 20px; right: 20px; padding: 8px 12px; border-radius: 6px;
|
979
|
+
font-size: 13px; font-weight: 600; z-index: 1000; max-width: 300px;
|
980
|
+
background: ${type === 'success' ? '#10b981' : '#ef4444'}; color: white;
|
981
|
+
animation: slideIn 0.3s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
982
|
+
`;
|
983
|
+
|
984
|
+
document.body.appendChild(toast);
|
985
|
+
setTimeout(() => toast.remove(), 4000);
|
986
|
+
}
|
987
|
+
|
988
|
+
// CSS animation
|
989
|
+
const style = document.createElement('style');
|
990
|
+
style.textContent = `
|
991
|
+
@keyframes slideIn {
|
992
|
+
from { transform: translateX(100%); opacity: 0; }
|
993
|
+
to { transform: translateX(0); opacity: 1; }
|
994
|
+
}
|
995
|
+
`;
|
996
|
+
document.head.appendChild(style);
|
997
|
+
</script>
|