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