pg_insights 0.1.0 → 0.2.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 +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 +69 -18
- data/lib/pg_insights/version.rb +1 -1
- data/lib/pg_insights.rb +24 -10
- data/lib/tasks/pg_insights.rake +419 -33
- metadata +7 -2
@@ -0,0 +1,797 @@
|
|
1
|
+
<div class="timeline-compact">
|
2
|
+
<!-- Compact Header -->
|
3
|
+
<div class="compact-header">
|
4
|
+
<div class="header-content">
|
5
|
+
<div class="header-main">
|
6
|
+
<h1>📊 Database Timeline</h1>
|
7
|
+
<p>PostgreSQL performance monitoring & configuration tracking</p>
|
8
|
+
</div>
|
9
|
+
<button onclick="refreshSnapshot()" class="btn-primary compact">🔄 Collect</button>
|
10
|
+
</div>
|
11
|
+
</div>
|
12
|
+
|
13
|
+
<!-- Compact Stats -->
|
14
|
+
<% if @stats.any? %>
|
15
|
+
<div class="stats-row">
|
16
|
+
<div class="stat-compact">
|
17
|
+
<div class="stat-icon">📊</div>
|
18
|
+
<div class="stat-data">
|
19
|
+
<span class="value"><%= @stats[:total_snapshots] %></span>
|
20
|
+
<span class="label">Snapshots</span>
|
21
|
+
</div>
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<div class="stat-compact <%= (@stats[:parameter_changes_count] || 0) > 0 ? 'warning' : 'success' %>">
|
25
|
+
<div class="stat-icon">⚙️</div>
|
26
|
+
<div class="stat-data">
|
27
|
+
<span class="value"><%= @stats[:parameter_changes_count] || 0 %></span>
|
28
|
+
<span class="label">Config Changes</span>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<div class="stat-compact <%= (@stats[:latest_cache_hit_rate] || 0) > 95 ? 'success' : 'warning' %>">
|
33
|
+
<div class="stat-icon">💾</div>
|
34
|
+
<div class="stat-data">
|
35
|
+
<span class="value"><%= (@stats[:latest_cache_hit_rate] || 0).round(1) %>%</span>
|
36
|
+
<span class="label">Cache Hit</span>
|
37
|
+
</div>
|
38
|
+
</div>
|
39
|
+
|
40
|
+
<div class="stat-compact">
|
41
|
+
<div class="stat-icon">📅</div>
|
42
|
+
<div class="stat-data">
|
43
|
+
<span class="value">
|
44
|
+
<% if @stats.dig(:date_range, :from) && @stats.dig(:date_range, :to) %>
|
45
|
+
<%= distance_of_time_in_words(@stats.dig(:date_range, :from), @stats.dig(:date_range, :to)) %>
|
46
|
+
<% else %>
|
47
|
+
--
|
48
|
+
<% end %>
|
49
|
+
</span>
|
50
|
+
<span class="label">Period</span>
|
51
|
+
</div>
|
52
|
+
</div>
|
53
|
+
|
54
|
+
<div class="actions-compact">
|
55
|
+
<a href="<%= timeline_export_path(format: 'csv') %>" class="btn-ghost">📥 CSV</a>
|
56
|
+
<a href="<%= timeline_export_path(format: 'json') %>" class="btn-ghost">📋 JSON</a>
|
57
|
+
<a href="#compare" class="btn-ghost" onclick="scrollToCompare()">🔍 Compare</a>
|
58
|
+
</div>
|
59
|
+
</div>
|
60
|
+
<% end %>
|
61
|
+
|
62
|
+
<!-- Main Layout -->
|
63
|
+
<div class="main-layout">
|
64
|
+
|
65
|
+
<!-- Chart Panel -->
|
66
|
+
<div class="chart-panel">
|
67
|
+
<div class="panel-header">
|
68
|
+
<h3>📈 Performance Timeline</h3>
|
69
|
+
<div class="chart-controls">
|
70
|
+
<div class="legend">
|
71
|
+
<span class="legend-item"><span class="dot green"></span>Cache Hit %</span>
|
72
|
+
<span class="legend-item"><span class="dot yellow"></span>Query Time (ms)</span>
|
73
|
+
</div>
|
74
|
+
<div class="chart-actions">
|
75
|
+
<button class="btn-mini">📊 Export</button>
|
76
|
+
</div>
|
77
|
+
</div>
|
78
|
+
</div>
|
79
|
+
|
80
|
+
<% if @timeline_data && @timeline_data[:dates]&.any? %>
|
81
|
+
<div class="chart-area">
|
82
|
+
<%= line_chart [
|
83
|
+
{ name: "Cache Hit Rate (%)", data: (@timeline_data[:dates] || []).zip(@timeline_data[:cache_hit_rates] || []) },
|
84
|
+
{ name: "Avg Query Time (ms)", data: (@timeline_data[:dates] || []).zip(@timeline_data[:avg_query_times] || []) }
|
85
|
+
], height: "280px", colors: ["#10b981", "#f59e0b"], curve: false, points: false %>
|
86
|
+
</div>
|
87
|
+
|
88
|
+
<!-- Compact Parameter Changes -->
|
89
|
+
<% if @timeline_data && @timeline_data[:parameter_changes]&.any? %>
|
90
|
+
<div class="changes-timeline">
|
91
|
+
<div class="changes-header">
|
92
|
+
<span class="changes-title">⚙️ Config Changes (<%= (@timeline_data[:parameter_changes] || []).count %>)</span>
|
93
|
+
</div>
|
94
|
+
<div class="changes-list">
|
95
|
+
<% @timeline_data[:parameter_changes].first(3).each_with_index do |change_event, index| %>
|
96
|
+
<div class="change-item <%= index == 0 ? 'recent' : '' %>">
|
97
|
+
<div class="change-date"><%= change_event[:detected_at].strftime('%m/%d %H:%M') %></div>
|
98
|
+
<div class="change-details">
|
99
|
+
<% (change_event[:changes] || {}).first(2).each do |param, details| %>
|
100
|
+
<span class="change-tag">
|
101
|
+
<code><%= param.split('_').first %></code>: <%= details[:from] %> → <%= details[:to] %>
|
102
|
+
</span>
|
103
|
+
<% end %>
|
104
|
+
<% if (change_event[:changes] || {}).count > 2 %>
|
105
|
+
<span class="change-more">+<%= (change_event[:changes] || {}).count - 2 %> more</span>
|
106
|
+
<% end %>
|
107
|
+
</div>
|
108
|
+
</div>
|
109
|
+
<% end %>
|
110
|
+
</div>
|
111
|
+
</div>
|
112
|
+
<% end %>
|
113
|
+
<% else %>
|
114
|
+
<div class="empty-chart">
|
115
|
+
<div class="empty-icon">📈</div>
|
116
|
+
<h4>No Timeline Data</h4>
|
117
|
+
<p>Collect snapshots to see performance trends</p>
|
118
|
+
<button onclick="refreshSnapshot()" class="btn-primary">🚀 Start</button>
|
119
|
+
</div>
|
120
|
+
<% end %>
|
121
|
+
</div>
|
122
|
+
|
123
|
+
<!-- Sidebar -->
|
124
|
+
<div class="sidebar-panel">
|
125
|
+
|
126
|
+
<!-- System Status -->
|
127
|
+
<div class="status-panel">
|
128
|
+
<h4>System Status</h4>
|
129
|
+
<div class="status-grid">
|
130
|
+
<div class="status-item">
|
131
|
+
<span class="status-dot green"></span>
|
132
|
+
<span class="status-text">Collection Active</span>
|
133
|
+
</div>
|
134
|
+
<div class="status-item">
|
135
|
+
<span class="status-dot green"></span>
|
136
|
+
<span class="status-text">DB Connected</span>
|
137
|
+
</div>
|
138
|
+
<div class="status-item">
|
139
|
+
<span class="status-dot blue"></span>
|
140
|
+
<span class="status-text">90 days retention</span>
|
141
|
+
</div>
|
142
|
+
</div>
|
143
|
+
</div>
|
144
|
+
|
145
|
+
<!-- Recent Snapshots -->
|
146
|
+
<div class="snapshots-panel">
|
147
|
+
<h4>📸 Recent Snapshots</h4>
|
148
|
+
<% if @snapshots.any? %>
|
149
|
+
<div class="snapshots-list">
|
150
|
+
<% @snapshots.first(5).each do |snapshot| %>
|
151
|
+
<div class="snapshot-row">
|
152
|
+
<div class="snapshot-time">
|
153
|
+
<%= snapshot.executed_at.strftime('%m/%d %H:%M') %>
|
154
|
+
<span class="status-mini <%= snapshot.status %>"></span>
|
155
|
+
</div>
|
156
|
+
<div class="snapshot-metrics">
|
157
|
+
<span class="metric">💾 <%= (snapshot.result_data.dig('metrics', 'cache_hit_rate') || 0).to_f.round(1) %>%</span>
|
158
|
+
<span class="metric">🔗 <%= (snapshot.result_data.dig('metrics', 'total_connections') || 0).to_i %></span>
|
159
|
+
</div>
|
160
|
+
<a href="<%= timeline_show_path(id: snapshot.id) %>" class="btn-mini">View</a>
|
161
|
+
</div>
|
162
|
+
<% end %>
|
163
|
+
</div>
|
164
|
+
<% if @snapshots.count > 5 %>
|
165
|
+
<div class="load-more">
|
166
|
+
<button class="btn-ghost small" onclick="loadMore()">+<%= @snapshots.count - 5 %> more</button>
|
167
|
+
</div>
|
168
|
+
<% end %>
|
169
|
+
<% else %>
|
170
|
+
<div class="empty-snapshots">
|
171
|
+
<p>No snapshots yet</p>
|
172
|
+
<button onclick="refreshSnapshot()" class="btn-primary small">Create First</button>
|
173
|
+
</div>
|
174
|
+
<% end %>
|
175
|
+
</div>
|
176
|
+
</div>
|
177
|
+
</div>
|
178
|
+
|
179
|
+
<!-- Compact Comparison Tool -->
|
180
|
+
<div class="compare-panel" id="compare">
|
181
|
+
<div class="panel-header">
|
182
|
+
<h3>🔍 Compare Snapshots</h3>
|
183
|
+
</div>
|
184
|
+
<div class="compare-form">
|
185
|
+
<form action="<%= timeline_compare_path %>" method="get" class="form-row">
|
186
|
+
<div class="form-group">
|
187
|
+
<label>From</label>
|
188
|
+
<input type="date" id="date1" name="date1" required>
|
189
|
+
</div>
|
190
|
+
<div class="form-group">
|
191
|
+
<label>To</label>
|
192
|
+
<input type="date" id="date2" name="date2" required>
|
193
|
+
</div>
|
194
|
+
<div class="form-actions">
|
195
|
+
<button type="button" class="btn-ghost" onclick="setRange(7)">7d</button>
|
196
|
+
<button type="button" class="btn-ghost" onclick="setRange(30)">30d</button>
|
197
|
+
<button type="submit" class="btn-primary">Compare</button>
|
198
|
+
</div>
|
199
|
+
</form>
|
200
|
+
</div>
|
201
|
+
</div>
|
202
|
+
</div>
|
203
|
+
|
204
|
+
<!-- Compact Design System -->
|
205
|
+
<style>
|
206
|
+
:root {
|
207
|
+
--space-xs: 4px;
|
208
|
+
--space-sm: 8px;
|
209
|
+
--space-md: 12px;
|
210
|
+
--space-lg: 16px;
|
211
|
+
--space-xl: 20px;
|
212
|
+
|
213
|
+
--text-xs: 11px;
|
214
|
+
--text-sm: 13px;
|
215
|
+
--text-base: 14px;
|
216
|
+
--text-lg: 16px;
|
217
|
+
--text-xl: 18px;
|
218
|
+
|
219
|
+
--success: #10b981;
|
220
|
+
--warning: #f59e0b;
|
221
|
+
--danger: #ef4444;
|
222
|
+
--info: #3b82f6;
|
223
|
+
--neutral: #6b7280;
|
224
|
+
|
225
|
+
--border: #e5e7eb;
|
226
|
+
--bg: #f9fafb;
|
227
|
+
--card: #ffffff;
|
228
|
+
--text: #111827;
|
229
|
+
--text-muted: #6b7280;
|
230
|
+
}
|
231
|
+
|
232
|
+
.timeline-compact {
|
233
|
+
max-width: 1400px;
|
234
|
+
margin: 0 auto;
|
235
|
+
padding: var(--space-lg);
|
236
|
+
background: var(--bg);
|
237
|
+
font-size: var(--text-base);
|
238
|
+
line-height: 1.4;
|
239
|
+
}
|
240
|
+
|
241
|
+
/* Compact Header */
|
242
|
+
.compact-header {
|
243
|
+
background: var(--card);
|
244
|
+
border: 1px solid var(--border);
|
245
|
+
border-radius: 8px;
|
246
|
+
padding: var(--space-lg);
|
247
|
+
margin-bottom: var(--space-lg);
|
248
|
+
}
|
249
|
+
|
250
|
+
.header-content {
|
251
|
+
display: flex;
|
252
|
+
justify-content: space-between;
|
253
|
+
align-items: center;
|
254
|
+
}
|
255
|
+
|
256
|
+
.compact-header h1 {
|
257
|
+
font-size: var(--text-xl);
|
258
|
+
font-weight: 700;
|
259
|
+
margin: 0 0 2px 0;
|
260
|
+
color: var(--text);
|
261
|
+
}
|
262
|
+
|
263
|
+
.compact-header p {
|
264
|
+
font-size: var(--text-sm);
|
265
|
+
color: var(--text-muted);
|
266
|
+
margin: 0;
|
267
|
+
}
|
268
|
+
|
269
|
+
/* Compact Stats Row */
|
270
|
+
.stats-row {
|
271
|
+
display: flex;
|
272
|
+
gap: var(--space-lg);
|
273
|
+
margin-bottom: var(--space-lg);
|
274
|
+
align-items: center;
|
275
|
+
flex-wrap: wrap;
|
276
|
+
}
|
277
|
+
|
278
|
+
.stat-compact {
|
279
|
+
background: var(--card);
|
280
|
+
border: 1px solid var(--border);
|
281
|
+
border-radius: 8px;
|
282
|
+
padding: var(--space-md) var(--space-lg);
|
283
|
+
display: flex;
|
284
|
+
align-items: center;
|
285
|
+
gap: var(--space-md);
|
286
|
+
min-width: 120px;
|
287
|
+
position: relative;
|
288
|
+
}
|
289
|
+
|
290
|
+
.stat-compact.success::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--success); border-radius: 8px 8px 0 0; }
|
291
|
+
.stat-compact.warning::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--warning); border-radius: 8px 8px 0 0; }
|
292
|
+
|
293
|
+
.stat-icon {
|
294
|
+
font-size: var(--text-lg);
|
295
|
+
}
|
296
|
+
|
297
|
+
.stat-data {
|
298
|
+
display: flex;
|
299
|
+
flex-direction: column;
|
300
|
+
gap: 1px;
|
301
|
+
}
|
302
|
+
|
303
|
+
.stat-data .value {
|
304
|
+
font-size: var(--text-lg);
|
305
|
+
font-weight: 700;
|
306
|
+
color: var(--text);
|
307
|
+
line-height: 1;
|
308
|
+
}
|
309
|
+
|
310
|
+
.stat-data .label {
|
311
|
+
font-size: var(--text-xs);
|
312
|
+
color: var(--text-muted);
|
313
|
+
line-height: 1;
|
314
|
+
}
|
315
|
+
|
316
|
+
.actions-compact {
|
317
|
+
display: flex;
|
318
|
+
gap: var(--space-sm);
|
319
|
+
margin-left: auto;
|
320
|
+
}
|
321
|
+
|
322
|
+
/* Main Layout */
|
323
|
+
.main-layout {
|
324
|
+
display: grid;
|
325
|
+
grid-template-columns: 1fr 320px;
|
326
|
+
gap: var(--space-lg);
|
327
|
+
margin-bottom: var(--space-lg);
|
328
|
+
}
|
329
|
+
|
330
|
+
/* Panel Base */
|
331
|
+
.chart-panel, .sidebar-panel, .compare-panel {
|
332
|
+
background: var(--card);
|
333
|
+
border: 1px solid var(--border);
|
334
|
+
border-radius: 8px;
|
335
|
+
overflow: hidden;
|
336
|
+
}
|
337
|
+
|
338
|
+
.panel-header {
|
339
|
+
padding: var(--space-lg) var(--space-lg) var(--space-md) var(--space-lg);
|
340
|
+
border-bottom: 1px solid var(--border);
|
341
|
+
display: flex;
|
342
|
+
justify-content: space-between;
|
343
|
+
align-items: center;
|
344
|
+
}
|
345
|
+
|
346
|
+
.panel-header h3, .panel-header h4 {
|
347
|
+
font-size: var(--text-lg);
|
348
|
+
font-weight: 600;
|
349
|
+
margin: 0;
|
350
|
+
color: var(--text);
|
351
|
+
}
|
352
|
+
|
353
|
+
/* Chart Panel */
|
354
|
+
.chart-controls {
|
355
|
+
display: flex;
|
356
|
+
align-items: center;
|
357
|
+
gap: var(--space-lg);
|
358
|
+
}
|
359
|
+
|
360
|
+
.legend {
|
361
|
+
display: flex;
|
362
|
+
gap: var(--space-lg);
|
363
|
+
}
|
364
|
+
|
365
|
+
.legend-item {
|
366
|
+
display: flex;
|
367
|
+
align-items: center;
|
368
|
+
gap: var(--space-xs);
|
369
|
+
font-size: var(--text-sm);
|
370
|
+
color: var(--text-muted);
|
371
|
+
}
|
372
|
+
|
373
|
+
.dot {
|
374
|
+
width: 8px;
|
375
|
+
height: 8px;
|
376
|
+
border-radius: 50%;
|
377
|
+
}
|
378
|
+
|
379
|
+
.dot.green { background: var(--success); }
|
380
|
+
.dot.yellow { background: var(--warning); }
|
381
|
+
|
382
|
+
.chart-area {
|
383
|
+
padding: var(--space-md) var(--space-lg);
|
384
|
+
}
|
385
|
+
|
386
|
+
/* Compact Changes Timeline */
|
387
|
+
.changes-timeline {
|
388
|
+
border-top: 1px solid var(--border);
|
389
|
+
padding: var(--space-lg);
|
390
|
+
}
|
391
|
+
|
392
|
+
.changes-header {
|
393
|
+
margin-bottom: var(--space-md);
|
394
|
+
}
|
395
|
+
|
396
|
+
.changes-title {
|
397
|
+
font-size: var(--text-base);
|
398
|
+
font-weight: 600;
|
399
|
+
color: var(--text);
|
400
|
+
}
|
401
|
+
|
402
|
+
.changes-list {
|
403
|
+
display: flex;
|
404
|
+
flex-direction: column;
|
405
|
+
gap: var(--space-sm);
|
406
|
+
}
|
407
|
+
|
408
|
+
.change-item {
|
409
|
+
display: flex;
|
410
|
+
gap: var(--space-md);
|
411
|
+
padding: var(--space-sm);
|
412
|
+
background: var(--bg);
|
413
|
+
border-radius: 6px;
|
414
|
+
border-left: 3px solid var(--border);
|
415
|
+
}
|
416
|
+
|
417
|
+
.change-item.recent {
|
418
|
+
border-left-color: var(--info);
|
419
|
+
background: #eff6ff;
|
420
|
+
}
|
421
|
+
|
422
|
+
.change-date {
|
423
|
+
font-size: var(--text-xs);
|
424
|
+
color: var(--text-muted);
|
425
|
+
font-weight: 600;
|
426
|
+
min-width: 60px;
|
427
|
+
}
|
428
|
+
|
429
|
+
.change-details {
|
430
|
+
display: flex;
|
431
|
+
flex-wrap: wrap;
|
432
|
+
gap: var(--space-xs);
|
433
|
+
align-items: center;
|
434
|
+
}
|
435
|
+
|
436
|
+
.change-tag {
|
437
|
+
background: var(--card);
|
438
|
+
border: 1px solid var(--border);
|
439
|
+
padding: 2px var(--space-xs);
|
440
|
+
border-radius: 4px;
|
441
|
+
font-size: var(--text-xs);
|
442
|
+
}
|
443
|
+
|
444
|
+
.change-tag code {
|
445
|
+
color: var(--info);
|
446
|
+
font-weight: 600;
|
447
|
+
}
|
448
|
+
|
449
|
+
.change-more {
|
450
|
+
font-size: var(--text-xs);
|
451
|
+
color: var(--text-muted);
|
452
|
+
font-style: italic;
|
453
|
+
}
|
454
|
+
|
455
|
+
/* Sidebar Panels */
|
456
|
+
.sidebar-panel {
|
457
|
+
display: flex;
|
458
|
+
flex-direction: column;
|
459
|
+
gap: var(--space-lg);
|
460
|
+
}
|
461
|
+
|
462
|
+
.status-panel, .snapshots-panel {
|
463
|
+
background: var(--card);
|
464
|
+
border: 1px solid var(--border);
|
465
|
+
border-radius: 8px;
|
466
|
+
padding: var(--space-lg);
|
467
|
+
}
|
468
|
+
|
469
|
+
.status-panel h4, .snapshots-panel h4 {
|
470
|
+
font-size: var(--text-base);
|
471
|
+
font-weight: 600;
|
472
|
+
margin: 0 0 var(--space-md) 0;
|
473
|
+
color: var(--text);
|
474
|
+
}
|
475
|
+
|
476
|
+
.status-grid {
|
477
|
+
display: flex;
|
478
|
+
flex-direction: column;
|
479
|
+
gap: var(--space-sm);
|
480
|
+
}
|
481
|
+
|
482
|
+
.status-item {
|
483
|
+
display: flex;
|
484
|
+
align-items: center;
|
485
|
+
gap: var(--space-sm);
|
486
|
+
}
|
487
|
+
|
488
|
+
.status-dot {
|
489
|
+
width: 6px;
|
490
|
+
height: 6px;
|
491
|
+
border-radius: 50%;
|
492
|
+
}
|
493
|
+
|
494
|
+
.status-dot.green { background: var(--success); }
|
495
|
+
.status-dot.blue { background: var(--info); }
|
496
|
+
|
497
|
+
.status-text {
|
498
|
+
font-size: var(--text-sm);
|
499
|
+
color: var(--text-muted);
|
500
|
+
}
|
501
|
+
|
502
|
+
/* Snapshots List */
|
503
|
+
.snapshots-list {
|
504
|
+
display: flex;
|
505
|
+
flex-direction: column;
|
506
|
+
gap: var(--space-sm);
|
507
|
+
}
|
508
|
+
|
509
|
+
.snapshot-row {
|
510
|
+
display: flex;
|
511
|
+
align-items: center;
|
512
|
+
gap: var(--space-sm);
|
513
|
+
padding: var(--space-sm);
|
514
|
+
background: var(--bg);
|
515
|
+
border-radius: 6px;
|
516
|
+
border: 1px solid var(--border);
|
517
|
+
}
|
518
|
+
|
519
|
+
.snapshot-time {
|
520
|
+
display: flex;
|
521
|
+
align-items: center;
|
522
|
+
gap: var(--space-xs);
|
523
|
+
font-size: var(--text-xs);
|
524
|
+
color: var(--text-muted);
|
525
|
+
font-weight: 600;
|
526
|
+
min-width: 80px;
|
527
|
+
}
|
528
|
+
|
529
|
+
.status-mini {
|
530
|
+
width: 4px;
|
531
|
+
height: 4px;
|
532
|
+
border-radius: 50%;
|
533
|
+
background: var(--success);
|
534
|
+
}
|
535
|
+
|
536
|
+
.snapshot-metrics {
|
537
|
+
display: flex;
|
538
|
+
gap: var(--space-sm);
|
539
|
+
flex: 1;
|
540
|
+
}
|
541
|
+
|
542
|
+
.metric {
|
543
|
+
font-size: var(--text-xs);
|
544
|
+
color: var(--text-muted);
|
545
|
+
}
|
546
|
+
|
547
|
+
/* Compare Panel */
|
548
|
+
.compare-panel {
|
549
|
+
margin-bottom: var(--space-lg);
|
550
|
+
}
|
551
|
+
|
552
|
+
.compare-form {
|
553
|
+
padding: var(--space-lg);
|
554
|
+
}
|
555
|
+
|
556
|
+
.form-row {
|
557
|
+
display: flex;
|
558
|
+
gap: var(--space-lg);
|
559
|
+
align-items: end;
|
560
|
+
}
|
561
|
+
|
562
|
+
.form-group {
|
563
|
+
display: flex;
|
564
|
+
flex-direction: column;
|
565
|
+
gap: var(--space-xs);
|
566
|
+
}
|
567
|
+
|
568
|
+
.form-group label {
|
569
|
+
font-size: var(--text-sm);
|
570
|
+
font-weight: 600;
|
571
|
+
color: var(--text);
|
572
|
+
}
|
573
|
+
|
574
|
+
.form-group input {
|
575
|
+
padding: var(--space-sm);
|
576
|
+
border: 1px solid var(--border);
|
577
|
+
border-radius: 6px;
|
578
|
+
font-size: var(--text-sm);
|
579
|
+
}
|
580
|
+
|
581
|
+
.form-actions {
|
582
|
+
display: flex;
|
583
|
+
gap: var(--space-sm);
|
584
|
+
align-items: center;
|
585
|
+
}
|
586
|
+
|
587
|
+
/* Buttons */
|
588
|
+
.btn-primary, .btn-ghost, .btn-mini {
|
589
|
+
border: none;
|
590
|
+
border-radius: 6px;
|
591
|
+
font-weight: 600;
|
592
|
+
cursor: pointer;
|
593
|
+
text-decoration: none;
|
594
|
+
display: inline-flex;
|
595
|
+
align-items: center;
|
596
|
+
justify-content: center;
|
597
|
+
transition: all 0.2s;
|
598
|
+
}
|
599
|
+
|
600
|
+
.btn-primary {
|
601
|
+
background: var(--info);
|
602
|
+
color: white;
|
603
|
+
padding: var(--space-sm) var(--space-lg);
|
604
|
+
font-size: var(--text-sm);
|
605
|
+
}
|
606
|
+
|
607
|
+
.btn-primary:hover {
|
608
|
+
background: #2563eb;
|
609
|
+
transform: translateY(-1px);
|
610
|
+
}
|
611
|
+
|
612
|
+
.btn-primary.compact {
|
613
|
+
padding: var(--space-sm) var(--space-md);
|
614
|
+
font-size: var(--text-sm);
|
615
|
+
}
|
616
|
+
|
617
|
+
.btn-primary.small {
|
618
|
+
padding: var(--space-xs) var(--space-sm);
|
619
|
+
font-size: var(--text-xs);
|
620
|
+
}
|
621
|
+
|
622
|
+
.btn-ghost {
|
623
|
+
background: transparent;
|
624
|
+
color: var(--text-muted);
|
625
|
+
border: 1px solid var(--border);
|
626
|
+
padding: var(--space-xs) var(--space-sm);
|
627
|
+
font-size: var(--text-sm);
|
628
|
+
}
|
629
|
+
|
630
|
+
.btn-ghost:hover {
|
631
|
+
background: var(--bg);
|
632
|
+
color: var(--text);
|
633
|
+
}
|
634
|
+
|
635
|
+
.btn-ghost.small {
|
636
|
+
padding: 2px var(--space-xs);
|
637
|
+
font-size: var(--text-xs);
|
638
|
+
}
|
639
|
+
|
640
|
+
.btn-mini {
|
641
|
+
background: var(--bg);
|
642
|
+
color: var(--text-muted);
|
643
|
+
border: 1px solid var(--border);
|
644
|
+
padding: 2px var(--space-xs);
|
645
|
+
font-size: var(--text-xs);
|
646
|
+
}
|
647
|
+
|
648
|
+
.btn-mini:hover {
|
649
|
+
background: var(--info);
|
650
|
+
color: white;
|
651
|
+
}
|
652
|
+
|
653
|
+
/* Empty States */
|
654
|
+
.empty-chart, .empty-snapshots {
|
655
|
+
text-align: center;
|
656
|
+
padding: var(--space-xl);
|
657
|
+
color: var(--text-muted);
|
658
|
+
}
|
659
|
+
|
660
|
+
.empty-icon {
|
661
|
+
font-size: 32px;
|
662
|
+
margin-bottom: var(--space-sm);
|
663
|
+
}
|
664
|
+
|
665
|
+
.empty-chart h4, .empty-snapshots h4 {
|
666
|
+
font-size: var(--text-base);
|
667
|
+
font-weight: 600;
|
668
|
+
margin: 0 0 var(--space-xs) 0;
|
669
|
+
color: var(--text);
|
670
|
+
}
|
671
|
+
|
672
|
+
.empty-chart p, .empty-snapshots p {
|
673
|
+
font-size: var(--text-sm);
|
674
|
+
margin: 0 0 var(--space-md) 0;
|
675
|
+
}
|
676
|
+
|
677
|
+
.load-more {
|
678
|
+
text-align: center;
|
679
|
+
padding-top: var(--space-md);
|
680
|
+
border-top: 1px solid var(--border);
|
681
|
+
margin-top: var(--space-md);
|
682
|
+
}
|
683
|
+
|
684
|
+
/* Responsive */
|
685
|
+
@media (max-width: 1200px) {
|
686
|
+
.main-layout {
|
687
|
+
grid-template-columns: 1fr;
|
688
|
+
}
|
689
|
+
|
690
|
+
.sidebar-panel {
|
691
|
+
flex-direction: row;
|
692
|
+
}
|
693
|
+
|
694
|
+
.status-panel, .snapshots-panel {
|
695
|
+
flex: 1;
|
696
|
+
}
|
697
|
+
}
|
698
|
+
|
699
|
+
@media (max-width: 768px) {
|
700
|
+
.timeline-compact {
|
701
|
+
padding: var(--space-md);
|
702
|
+
}
|
703
|
+
|
704
|
+
.stats-row {
|
705
|
+
display: grid;
|
706
|
+
grid-template-columns: repeat(2, 1fr);
|
707
|
+
gap: var(--space-sm);
|
708
|
+
}
|
709
|
+
|
710
|
+
.actions-compact {
|
711
|
+
grid-column: 1 / -1;
|
712
|
+
justify-content: center;
|
713
|
+
margin: var(--space-md) 0 0 0;
|
714
|
+
}
|
715
|
+
|
716
|
+
.form-row {
|
717
|
+
flex-direction: column;
|
718
|
+
align-items: stretch;
|
719
|
+
}
|
720
|
+
|
721
|
+
.sidebar-panel {
|
722
|
+
flex-direction: column;
|
723
|
+
}
|
724
|
+
}
|
725
|
+
</style>
|
726
|
+
|
727
|
+
<script>
|
728
|
+
function refreshSnapshot() {
|
729
|
+
const button = event.target;
|
730
|
+
const originalText = button.innerHTML;
|
731
|
+
button.innerHTML = '⏳ Collecting...';
|
732
|
+
button.disabled = true;
|
733
|
+
|
734
|
+
fetch('<%= timeline_refresh_path %>', {
|
735
|
+
method: 'POST',
|
736
|
+
headers: {
|
737
|
+
'Content-Type': 'application/json',
|
738
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
739
|
+
}
|
740
|
+
})
|
741
|
+
.then(response => response.json())
|
742
|
+
.then(data => {
|
743
|
+
button.innerHTML = originalText;
|
744
|
+
button.disabled = false;
|
745
|
+
|
746
|
+
if (data.message) {
|
747
|
+
showToast('✅ ' + data.message, 'success');
|
748
|
+
setTimeout(() => location.reload(), 2000);
|
749
|
+
} else if (data.error) {
|
750
|
+
showToast('❌ ' + data.error, 'error');
|
751
|
+
}
|
752
|
+
})
|
753
|
+
.catch(error => {
|
754
|
+
button.innerHTML = originalText;
|
755
|
+
button.disabled = false;
|
756
|
+
showToast('❌ Error: ' + error.message, 'error');
|
757
|
+
});
|
758
|
+
}
|
759
|
+
|
760
|
+
function setRange(days) {
|
761
|
+
const today = new Date();
|
762
|
+
const pastDate = new Date(today.getTime() - days * 24 * 60 * 60 * 1000);
|
763
|
+
|
764
|
+
document.getElementById('date1').value = pastDate.toISOString().split('T')[0];
|
765
|
+
document.getElementById('date2').value = today.toISOString().split('T')[0];
|
766
|
+
}
|
767
|
+
|
768
|
+
function scrollToCompare() {
|
769
|
+
document.getElementById('compare').scrollIntoView({
|
770
|
+
behavior: 'smooth',
|
771
|
+
block: 'start'
|
772
|
+
});
|
773
|
+
}
|
774
|
+
|
775
|
+
function loadMore() {
|
776
|
+
console.log('Load more snapshots');
|
777
|
+
}
|
778
|
+
|
779
|
+
function showToast(message, type) {
|
780
|
+
const toast = document.createElement('div');
|
781
|
+
toast.innerHTML = message;
|
782
|
+
toast.style.cssText = `
|
783
|
+
position: fixed; top: 20px; right: 20px; padding: 8px 12px; border-radius: 6px;
|
784
|
+
font-size: 13px; font-weight: 600; z-index: 1000; max-width: 300px;
|
785
|
+
background: ${type === 'success' ? '#10b981' : '#ef4444'}; color: white;
|
786
|
+
animation: slideIn 0.3s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
787
|
+
`;
|
788
|
+
|
789
|
+
document.body.appendChild(toast);
|
790
|
+
setTimeout(() => toast.remove(), 4000);
|
791
|
+
}
|
792
|
+
|
793
|
+
// Set default dates
|
794
|
+
document.addEventListener('DOMContentLoaded', function() {
|
795
|
+
setRange(7);
|
796
|
+
});
|
797
|
+
</script>
|