solidstats 0.0.4 โ 1.1.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/CHANGELOG.md +78 -5
- data/README.md +54 -5
- data/app/controllers/solidstats/dashboard_controller.rb +60 -121
- data/app/services/solidstats/audit_service.rb +56 -0
- data/app/services/solidstats/data_collector_service.rb +83 -0
- data/app/services/solidstats/log_size_monitor_service.rb +94 -0
- data/app/services/solidstats/todo_service.rb +114 -0
- data/app/views/solidstats/dashboard/_log_monitor.html.erb +759 -0
- data/app/views/solidstats/dashboard/_todos.html.erb +6 -27
- data/app/views/solidstats/dashboard/audit/_additional_styles.css +22 -0
- data/app/views/solidstats/dashboard/audit/_audit_details.html.erb +47 -58
- data/app/views/solidstats/dashboard/audit/_audit_summary.html.erb +1 -1
- data/app/views/solidstats/dashboard/audit/_security_audit.html.erb +0 -23
- data/app/views/solidstats/dashboard/audit/_vulnerabilities_table.html.erb +1092 -38
- data/app/views/solidstats/dashboard/audit/_vulnerability_details.html.erb +4 -3
- data/app/views/solidstats/dashboard/index.html.erb +1166 -162
- data/config/routes.rb +5 -0
- data/lib/solidstats/version.rb +1 -1
- data/lib/tasks/solidstats_release.rake +69 -0
- metadata +12 -5
- data/app/views/solidstats/dashboard/audit/_audit_filters.html.erb +0 -5
@@ -1,46 +1,328 @@
|
|
1
1
|
<div class="solidstats-dashboard">
|
2
2
|
<header class="dashboard-header">
|
3
|
-
<
|
4
|
-
|
3
|
+
<div class="header-main">
|
4
|
+
<h1><span class="icon">๐ฅ</span> Solidstats Dashboard</h1>
|
5
|
+
<% created_at = @audit_output.dig("created_at") %>
|
6
|
+
<span class="audit-date">Last updated: <%= created_at ? DateTime.parse(created_at).strftime("%B %d, %Y at %H:%M") : Time.now.strftime("%B %d, %Y at %H:%M") %></span>
|
7
|
+
</div>
|
8
|
+
|
9
|
+
<nav class="dashboard-nav">
|
10
|
+
<ul>
|
11
|
+
<li><a href="#overview" class="nav-item active" data-section="overview">Overview</a></li>
|
12
|
+
<li><a href="#security" class="nav-item" data-section="security">Security</a></li>
|
13
|
+
<li><a href="#code-quality" class="nav-item" data-section="code-quality">Code Quality</a></li>
|
14
|
+
<li><a href="#tasks" class="nav-item" data-section="tasks">Tasks</a></li>
|
15
|
+
</ul>
|
16
|
+
|
17
|
+
<div class="dashboard-actions">
|
18
|
+
<a href="#" class="action-button" onclick="refreshAudit(); return false;">
|
19
|
+
<span class="action-icon">โป</span> Refresh
|
20
|
+
</a>
|
21
|
+
<button class="action-button" disabled title="Export is currently disabled" style="cursor: not-allowed;">
|
22
|
+
<span class="action-icon">โ</span> Export
|
23
|
+
</button>
|
24
|
+
</div>
|
25
|
+
</nav>
|
5
26
|
</header>
|
6
27
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
28
|
+
<!-- Overview Section with Key Metrics -->
|
29
|
+
<section id="overview" class="dashboard-section active">
|
30
|
+
<h2 class="section-title">Overview</h2>
|
31
|
+
|
32
|
+
<div class="stats-summary">
|
33
|
+
<div class="summary-card <%= @audit_output.dig('results').present? ? 'status-warning' : 'status-ok' %>" data-section="security" data-tab="security-overview">
|
34
|
+
<div class="summary-icon">๐</div>
|
35
|
+
<div class="summary-data">
|
36
|
+
<div class="summary-value"><%= @audit_output.dig('results')&.size || 0 %></div>
|
37
|
+
<div class="summary-label">Security Issues</div>
|
38
|
+
</div>
|
39
|
+
</div>
|
40
|
+
|
41
|
+
<div class="summary-card <%= @todo_items&.present? ? 'status-warning' : 'status-ok' %>" data-section="tasks" data-tab="todos">
|
42
|
+
<div class="summary-icon">๐</div>
|
43
|
+
<div class="summary-data">
|
44
|
+
<div class="summary-value"><%= @todo_items&.count || 0 %></div>
|
45
|
+
<div class="summary-label">TODO Items</div>
|
46
|
+
</div>
|
47
|
+
</div>
|
48
|
+
|
49
|
+
<div class="summary-card <%= @coverage.to_f > 80 ? 'status-ok' : (@coverage.to_f > 60 ? 'status-warning' : 'status-danger') %>" data-section="code-quality" data-tab="test-coverage">
|
50
|
+
<div class="summary-icon">๐งช</div>
|
51
|
+
<div class="summary-data">
|
52
|
+
<div class="summary-value"><%= @coverage %>%</div>
|
53
|
+
<div class="summary-label">Test Coverage</div>
|
54
|
+
</div>
|
55
|
+
</div>
|
56
|
+
|
57
|
+
<div class="summary-card status-<%= @log_data[:status] %>" data-section="code-quality" data-tab="log-monitor">
|
58
|
+
<div class="summary-icon">๐</div>
|
59
|
+
<div class="summary-data">
|
60
|
+
<div class="summary-value"><%= @log_data[:total_size_mb] %> MB</div>
|
61
|
+
<div class="summary-label">Log Files Size</div>
|
62
|
+
</div>
|
63
|
+
</div>
|
64
|
+
</div>
|
65
|
+
</section>
|
11
66
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
67
|
+
<!-- Security Section -->
|
68
|
+
<section id="security" class="dashboard-section">
|
69
|
+
<h2 class="section-title">Security</h2>
|
70
|
+
|
71
|
+
<div class="security-overview">
|
72
|
+
<% results = @audit_output.dig("results") || [] %>
|
73
|
+
<% vulnerabilities_count = results.size %>
|
74
|
+
<% high_severity = results.count { |r| %w[high critical].include?(r.dig("advisory", "criticality").to_s.downcase) } %>
|
75
|
+
<% affected_gems = results.map { |r| r.dig("gem", "name") }.uniq.size %>
|
76
|
+
|
77
|
+
<div class="security-score-container">
|
78
|
+
<div class="security-score <%= vulnerabilities_count == 0 ? 'score-excellent' : (high_severity > 0 ? 'score-critical' : 'score-warning') %>">
|
79
|
+
<div class="score-value"><%= vulnerabilities_count == 0 ? 'A+' : (high_severity > 0 ? 'C' : 'B') %></div>
|
80
|
+
<div class="score-label">Security<br>Rating</div>
|
81
|
+
</div>
|
82
|
+
|
83
|
+
<div class="security-metrics">
|
84
|
+
<div class="metric-item <%= high_severity > 0 ? 'metric-critical' : '' %>">
|
85
|
+
<div class="metric-icon">โ ๏ธ</div>
|
86
|
+
<div class="metric-data">
|
87
|
+
<div class="metric-value"><%= high_severity %></div>
|
88
|
+
<div class="metric-label">Critical Issues</div>
|
89
|
+
</div>
|
90
|
+
</div>
|
91
|
+
|
92
|
+
<div class="metric-item <%= vulnerabilities_count > 0 ? 'metric-warning' : '' %>">
|
93
|
+
<div class="metric-icon">๐</div>
|
94
|
+
<div class="metric-data">
|
95
|
+
<div class="metric-value"><%= vulnerabilities_count %></div>
|
96
|
+
<div class="metric-label">Total Vulnerabilities</div>
|
97
|
+
</div>
|
98
|
+
</div>
|
99
|
+
|
100
|
+
<div class="metric-item <%= affected_gems > 0 ? 'metric-warning' : '' %>">
|
101
|
+
<div class="metric-icon">๐</div>
|
102
|
+
<div class="metric-data">
|
103
|
+
<div class="metric-value"><%= affected_gems %></div>
|
104
|
+
<div class="metric-label">Affected Gems</div>
|
105
|
+
</div>
|
106
|
+
</div>
|
17
107
|
</div>
|
18
108
|
</div>
|
19
109
|
</div>
|
110
|
+
|
111
|
+
<div class="tabs-container">
|
112
|
+
<div class="tabs-header security-tabs">
|
113
|
+
<button class="tab-button active" data-tab="security-overview">
|
114
|
+
<span class="tab-icon">๐</span> Overview
|
115
|
+
</button>
|
116
|
+
<button class="tab-button" data-tab="security-gems">
|
117
|
+
<span class="tab-icon">๐</span> Affected Gems
|
118
|
+
</button>
|
119
|
+
<button class="tab-button" data-tab="security-timeline">
|
120
|
+
<span class="tab-icon">๐</span> Timeline
|
121
|
+
</button>
|
122
|
+
</div>
|
123
|
+
|
124
|
+
<div class="tabs-content">
|
125
|
+
<div class="tab-content active" id="security-overview">
|
126
|
+
<%= render partial: 'solidstats/dashboard/audit/security_audit', locals: { results: results } %>
|
127
|
+
</div>
|
128
|
+
<div class="tab-content" id="security-gems">
|
129
|
+
<div class="gem-impact-analysis">
|
130
|
+
<h3>Gem Impact Analysis</h3>
|
131
|
+
<% results = @audit_output.dig("results") || [] %>
|
132
|
+
<% if results.any? %>
|
133
|
+
<div class="gems-container">
|
134
|
+
<% results.map { |r| r.dig("gem", "name") }.uniq.each do |gem_name| %>
|
135
|
+
<% gem_vulns = results.select { |r| r.dig("gem", "name") == gem_name } %>
|
136
|
+
<% highest_severity = gem_vulns.map { |v| v.dig("advisory", "criticality").to_s.downcase }.select { |c| %w[critical high medium low].include?(c) }.min_by { |s| %w[critical high medium low].index(s) || 999 } || "unknown" %>
|
137
|
+
|
138
|
+
<div class="gem-card severity-<%= highest_severity %>">
|
139
|
+
<div class="gem-header">
|
140
|
+
<div class="gem-name"><%= gem_name %></div>
|
141
|
+
<div class="gem-severity <%= highest_severity %>"><%= highest_severity.capitalize %></div>
|
142
|
+
</div>
|
143
|
+
<div class="gem-details">
|
144
|
+
<div class="gem-versions">
|
145
|
+
<div class="current-version">
|
146
|
+
<span class="version-label">Current:</span>
|
147
|
+
<span class="version-value"><%= gem_vulns.first.dig("gem", "version") rescue "Unknown" %></span>
|
148
|
+
</div>
|
149
|
+
<div class="target-version">
|
150
|
+
<span class="version-label">Target:</span>
|
151
|
+
<span class="version-value"><%= gem_vulns.first.dig("advisory", "patched_versions")&.first || "N/A" %></span>
|
152
|
+
</div>
|
153
|
+
</div>
|
154
|
+
<div class="gem-vulnerabilities-count">
|
155
|
+
<%= gem_vulns.size %> <%= "vulnerability".pluralize(gem_vulns.size) %> found
|
156
|
+
</div>
|
157
|
+
</div>
|
158
|
+
<div class="gem-actions">
|
159
|
+
<button class="action-button gem-update-button">
|
160
|
+
<span class="action-icon">โ</span> Update Gem
|
161
|
+
</button>
|
162
|
+
</div>
|
163
|
+
</div>
|
164
|
+
<% end %>
|
165
|
+
</div>
|
166
|
+
<% else %>
|
167
|
+
<div class="empty-state">
|
168
|
+
<div class="empty-icon">โ
</div>
|
169
|
+
<div class="empty-message">No vulnerable gems found</div>
|
170
|
+
<div class="empty-description">Your application is secure. Keep up with regular security audits!</div>
|
171
|
+
</div>
|
172
|
+
<% end %>
|
173
|
+
</div>
|
174
|
+
</div>
|
175
|
+
<div class="tab-content" id="security-timeline">
|
176
|
+
<div class="security-timeline-container">
|
177
|
+
<h3>Security Timeline</h3>
|
178
|
+
<div class="timeline-chart-placeholder">
|
179
|
+
<div class="chart-header">
|
180
|
+
<div class="chart-title">Vulnerability History</div>
|
181
|
+
<div class="chart-legend">
|
182
|
+
<div class="legend-item">
|
183
|
+
<span class="legend-color" style="background-color: #dc3545;"></span>
|
184
|
+
<span class="legend-label">Critical</span>
|
185
|
+
</div>
|
186
|
+
<div class="legend-item">
|
187
|
+
<span class="legend-color" style="background-color: #ffc107;"></span>
|
188
|
+
<span class="legend-label">Medium</span>
|
189
|
+
</div>
|
190
|
+
<div class="legend-item">
|
191
|
+
<span class="legend-color" style="background-color: #28a745;"></span>
|
192
|
+
<span class="legend-label">Low</span>
|
193
|
+
</div>
|
194
|
+
</div>
|
195
|
+
</div>
|
196
|
+
<div class="chart-visualization">
|
197
|
+
<!-- Placeholder for the actual chart -->
|
198
|
+
<div class="chart-timeline">
|
199
|
+
<div class="timeline-point" style="left: 10%;">
|
200
|
+
<div class="timeline-marker critical"></div>
|
201
|
+
<div class="timeline-date">Jan 2025</div>
|
202
|
+
</div>
|
203
|
+
<div class="timeline-point" style="left: 30%;">
|
204
|
+
<div class="timeline-marker medium"></div>
|
205
|
+
<div class="timeline-date">Feb 2025</div>
|
206
|
+
</div>
|
207
|
+
<div class="timeline-point" style="left: 65%;">
|
208
|
+
<div class="timeline-marker low"></div>
|
209
|
+
<div class="timeline-date">Apr 2025</div>
|
210
|
+
</div>
|
211
|
+
<div class="timeline-point" style="left: 85%;">
|
212
|
+
<div class="timeline-marker critical"></div>
|
213
|
+
<div class="timeline-date">May 2025</div>
|
214
|
+
</div>
|
215
|
+
</div>
|
216
|
+
</div>
|
217
|
+
</div>
|
218
|
+
<div class="timeline-insights">
|
219
|
+
<div class="insight-card">
|
220
|
+
<div class="insight-header">Key Insights</div>
|
221
|
+
<div class="insight-content">
|
222
|
+
<div class="insight-item">
|
223
|
+
<div class="insight-title">Notable Trend</div>
|
224
|
+
<div class="insight-description">4 vulnerabilities discovered in the last 3 months.</div>
|
225
|
+
</div>
|
226
|
+
<div class="insight-item">
|
227
|
+
<div class="insight-title">Recent Activity</div>
|
228
|
+
<div class="insight-description">Last security scan: <%= Time.now.strftime("%B %d, %Y") %></div>
|
229
|
+
</div>
|
230
|
+
</div>
|
231
|
+
</div>
|
232
|
+
</div>
|
233
|
+
</div>
|
234
|
+
</div>
|
235
|
+
</div>
|
236
|
+
</div>
|
237
|
+
</section>
|
20
238
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
239
|
+
<!-- Code Quality Section -->
|
240
|
+
<section id="code-quality" class="dashboard-section">
|
241
|
+
<h2 class="section-title">Code Quality</h2>
|
242
|
+
|
243
|
+
<div class="tabs-container">
|
244
|
+
<div class="tabs-header">
|
245
|
+
<button class="tab-button active" data-tab="quality-metrics">Metrics</button>
|
246
|
+
<button class="tab-button" data-tab="test-coverage">Test Coverage</button>
|
247
|
+
<button class="tab-button" data-tab="code-health">Code Health</button>
|
248
|
+
<button class="tab-button" data-tab="log-monitor">
|
249
|
+
<span class="tab-icon">๐</span> Log Monitor
|
250
|
+
</button>
|
251
|
+
</div>
|
252
|
+
|
253
|
+
<div class="tabs-content">
|
254
|
+
<div class="tab-content active" id="quality-metrics">
|
255
|
+
<div class="stat-card">
|
256
|
+
<h2><span class="icon">๐งน</span> Code Quality</h2>
|
257
|
+
<div class="card-content">
|
258
|
+
<div class="metric">
|
259
|
+
<!-- Your code quality metrics -->
|
260
|
+
</div>
|
261
|
+
</div>
|
262
|
+
</div>
|
263
|
+
</div>
|
264
|
+
<div class="tab-content" id="test-coverage">
|
265
|
+
<div class="stat-card <%= @coverage.to_f > 80 ? 'status-ok' : (@coverage.to_f > 60 ? 'status-warning' : 'status-danger') %>">
|
266
|
+
<h2><span class="icon">๐งช</span> Test Coverage</h2>
|
267
|
+
<div class="card-content">
|
268
|
+
<div class="progress-container">
|
269
|
+
<div class="progress-bar" style="width: <%= @coverage %>%"></div>
|
270
|
+
</div>
|
271
|
+
<div class="metric">
|
272
|
+
<span class="metric-value"><%= @coverage %>%</span>
|
273
|
+
</div>
|
274
|
+
</div>
|
275
|
+
</div>
|
276
|
+
</div>
|
277
|
+
<div class="tab-content" id="code-health">
|
278
|
+
<div class="stat-card">
|
279
|
+
<h2><span class="icon">๐</span> Code Health</h2>
|
280
|
+
<div class="card-content">
|
281
|
+
<!-- Your code health metrics -->
|
282
|
+
</div>
|
283
|
+
</div>
|
284
|
+
</div>
|
285
|
+
<div class="tab-content" id="log-monitor">
|
286
|
+
<%= render partial: 'solidstats/dashboard/log_monitor' %>
|
287
|
+
</div>
|
25
288
|
</div>
|
26
289
|
</div>
|
290
|
+
</section>
|
27
291
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
292
|
+
<!-- Tasks Section -->
|
293
|
+
<section id="tasks" class="dashboard-section">
|
294
|
+
<h2 class="section-title">Tasks</h2>
|
295
|
+
|
296
|
+
<div class="tabs-container">
|
297
|
+
<div class="tabs-header">
|
298
|
+
<button class="tab-button active" data-tab="todos">TODO Items</button>
|
299
|
+
<button class="tab-button" data-tab="fixmes">FIXMEs</button>
|
300
|
+
<button class="tab-button" data-tab="hacks">HACKs</button>
|
301
|
+
</div>
|
302
|
+
|
303
|
+
<div class="tabs-content">
|
304
|
+
<div class="tab-content active" id="todos">
|
305
|
+
<%= render partial: 'todos' %>
|
33
306
|
</div>
|
34
|
-
<div class="
|
35
|
-
|
307
|
+
<div class="tab-content" id="fixmes">
|
308
|
+
<!-- FIXME specific content -->
|
309
|
+
</div>
|
310
|
+
<div class="tab-content" id="hacks">
|
311
|
+
<!-- HACK specific content -->
|
36
312
|
</div>
|
37
313
|
</div>
|
38
314
|
</div>
|
39
|
-
</
|
315
|
+
</section>
|
40
316
|
|
41
|
-
|
42
|
-
|
43
|
-
<
|
317
|
+
<!-- Floating quick navigation -->
|
318
|
+
<div class="quick-nav">
|
319
|
+
<button class="quick-nav-toggle">โ</button>
|
320
|
+
<div class="quick-nav-menu">
|
321
|
+
<a href="#overview" class="quick-nav-item">Overview</a>
|
322
|
+
<a href="#security" class="quick-nav-item">Security</a>
|
323
|
+
<a href="#code-quality" class="quick-nav-item">Code Quality</a>
|
324
|
+
<a href="#tasks" class="quick-nav-item">Tasks</a>
|
325
|
+
</div>
|
44
326
|
</div>
|
45
327
|
</div>
|
46
328
|
|
@@ -54,15 +336,28 @@
|
|
54
336
|
padding: 20px;
|
55
337
|
}
|
56
338
|
|
57
|
-
/* Header styles */
|
339
|
+
/* Header and Navigation styles */
|
58
340
|
.dashboard-header {
|
59
341
|
margin-bottom: 2rem;
|
342
|
+
position: sticky;
|
343
|
+
top: 0;
|
344
|
+
z-index: 100;
|
345
|
+
background-color: #fff;
|
346
|
+
padding: 15px 0;
|
347
|
+
border-bottom: 1px solid #eaeaea;
|
348
|
+
}
|
349
|
+
|
350
|
+
.header-main {
|
351
|
+
display: flex;
|
352
|
+
justify-content: space-between;
|
353
|
+
align-items: baseline;
|
354
|
+
margin-bottom: 1rem;
|
60
355
|
}
|
61
356
|
|
62
357
|
.dashboard-header h1 {
|
63
358
|
font-size: 1.8rem;
|
64
359
|
font-weight: 600;
|
65
|
-
margin: 0
|
360
|
+
margin: 0;
|
66
361
|
}
|
67
362
|
|
68
363
|
.dashboard-last-updated {
|
@@ -71,7 +366,150 @@
|
|
71
366
|
font-size: 0.9rem;
|
72
367
|
}
|
73
368
|
|
74
|
-
|
369
|
+
.dashboard-nav {
|
370
|
+
display: flex;
|
371
|
+
justify-content: space-between;
|
372
|
+
align-items: center;
|
373
|
+
}
|
374
|
+
|
375
|
+
.dashboard-nav ul {
|
376
|
+
display: flex;
|
377
|
+
list-style-type: none;
|
378
|
+
margin: 0;
|
379
|
+
padding: 0;
|
380
|
+
gap: 1rem;
|
381
|
+
}
|
382
|
+
|
383
|
+
.nav-item {
|
384
|
+
padding: 0.5rem 1rem;
|
385
|
+
text-decoration: none;
|
386
|
+
color: #555;
|
387
|
+
border-radius: 4px;
|
388
|
+
font-weight: 500;
|
389
|
+
transition: all 0.2s;
|
390
|
+
}
|
391
|
+
|
392
|
+
.nav-item:hover {
|
393
|
+
background-color: #f5f5f5;
|
394
|
+
color: #000;
|
395
|
+
}
|
396
|
+
|
397
|
+
.nav-item.active {
|
398
|
+
background-color: #e9f5ff;
|
399
|
+
color: #0366d6;
|
400
|
+
}
|
401
|
+
|
402
|
+
/* Section styling */
|
403
|
+
.dashboard-section {
|
404
|
+
margin-bottom: 3rem;
|
405
|
+
display: none;
|
406
|
+
}
|
407
|
+
|
408
|
+
.dashboard-section.active {
|
409
|
+
display: block;
|
410
|
+
}
|
411
|
+
|
412
|
+
.section-title {
|
413
|
+
font-size: 1.5rem;
|
414
|
+
margin-bottom: 1.5rem;
|
415
|
+
padding-bottom: 0.5rem;
|
416
|
+
border-bottom: 1px solid #eee;
|
417
|
+
}
|
418
|
+
|
419
|
+
/* Overview summary cards */
|
420
|
+
.stats-summary {
|
421
|
+
display: grid;
|
422
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
423
|
+
gap: 1rem;
|
424
|
+
margin-bottom: 2rem;
|
425
|
+
}
|
426
|
+
|
427
|
+
.summary-card {
|
428
|
+
display: flex;
|
429
|
+
align-items: center;
|
430
|
+
padding: 1.5rem;
|
431
|
+
background: #fff;
|
432
|
+
border-radius: 8px;
|
433
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
434
|
+
transition: transform 0.2s;
|
435
|
+
cursor: pointer;
|
436
|
+
}
|
437
|
+
|
438
|
+
.summary-card:hover {
|
439
|
+
transform: translateY(-2px);
|
440
|
+
}
|
441
|
+
|
442
|
+
.summary-icon {
|
443
|
+
font-size: 2rem;
|
444
|
+
margin-right: 1rem;
|
445
|
+
}
|
446
|
+
|
447
|
+
.summary-data {
|
448
|
+
flex-grow: 1;
|
449
|
+
}
|
450
|
+
|
451
|
+
.summary-value {
|
452
|
+
font-size: 1.8rem;
|
453
|
+
font-weight: 700;
|
454
|
+
line-height: 1;
|
455
|
+
margin-bottom: 0.25rem;
|
456
|
+
}
|
457
|
+
|
458
|
+
.summary-label {
|
459
|
+
color: #666;
|
460
|
+
font-size: 0.9rem;
|
461
|
+
}
|
462
|
+
|
463
|
+
/* Tabs container */
|
464
|
+
.tabs-container {
|
465
|
+
background: #fff;
|
466
|
+
border-radius: 8px;
|
467
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
468
|
+
overflow: hidden;
|
469
|
+
}
|
470
|
+
|
471
|
+
.tabs-header {
|
472
|
+
display: flex;
|
473
|
+
background-color: #f8f9fa;
|
474
|
+
border-bottom: 1px solid #dee2e6;
|
475
|
+
overflow-x: auto;
|
476
|
+
}
|
477
|
+
|
478
|
+
.tab-button {
|
479
|
+
padding: 0.75rem 1.25rem;
|
480
|
+
background: none;
|
481
|
+
border: none;
|
482
|
+
font-size: 0.9rem;
|
483
|
+
font-weight: 500;
|
484
|
+
color: #555;
|
485
|
+
cursor: pointer;
|
486
|
+
white-space: nowrap;
|
487
|
+
}
|
488
|
+
|
489
|
+
.tab-button:hover {
|
490
|
+
color: #000;
|
491
|
+
background-color: #f1f1f1;
|
492
|
+
}
|
493
|
+
|
494
|
+
.tab-button.active {
|
495
|
+
color: #0366d6;
|
496
|
+
border-bottom: 2px solid #0366d6;
|
497
|
+
background-color: white;
|
498
|
+
}
|
499
|
+
|
500
|
+
.tabs-content {
|
501
|
+
padding: 1.5rem;
|
502
|
+
}
|
503
|
+
|
504
|
+
.tab-content {
|
505
|
+
display: none;
|
506
|
+
}
|
507
|
+
|
508
|
+
.tab-content.active {
|
509
|
+
display: block;
|
510
|
+
}
|
511
|
+
|
512
|
+
/* Card grid layout - modified for tabs */
|
75
513
|
.stats-cards {
|
76
514
|
display: grid;
|
77
515
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
@@ -79,23 +517,97 @@
|
|
79
517
|
margin-bottom: 2rem;
|
80
518
|
}
|
81
519
|
|
520
|
+
/* Quick navigation floating button */
|
521
|
+
.quick-nav {
|
522
|
+
position: fixed;
|
523
|
+
bottom: 2rem;
|
524
|
+
right: 2rem;
|
525
|
+
z-index: 99;
|
526
|
+
}
|
527
|
+
|
528
|
+
.quick-nav-toggle {
|
529
|
+
width: 50px;
|
530
|
+
height: 50px;
|
531
|
+
border-radius: 50%;
|
532
|
+
background-color: #0366d6;
|
533
|
+
color: white;
|
534
|
+
border: none;
|
535
|
+
font-size: 1.5rem;
|
536
|
+
cursor: pointer;
|
537
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
538
|
+
transition: all 0.3s;
|
539
|
+
}
|
540
|
+
|
541
|
+
.quick-nav-toggle:hover {
|
542
|
+
transform: scale(1.05);
|
543
|
+
}
|
544
|
+
|
545
|
+
.quick-nav-menu {
|
546
|
+
position: absolute;
|
547
|
+
bottom: 60px;
|
548
|
+
right: 0;
|
549
|
+
background-color: white;
|
550
|
+
border-radius: 8px;
|
551
|
+
width: 150px;
|
552
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
553
|
+
display: none;
|
554
|
+
flex-direction: column;
|
555
|
+
}
|
556
|
+
|
557
|
+
.quick-nav-item {
|
558
|
+
padding: 0.75rem 1rem;
|
559
|
+
text-decoration: none;
|
560
|
+
color: #333;
|
561
|
+
transition: background-color 0.2s;
|
562
|
+
}
|
563
|
+
|
564
|
+
.quick-nav-item:hover {
|
565
|
+
background-color: #f5f5f5;
|
566
|
+
}
|
567
|
+
|
568
|
+
.quick-nav:hover .quick-nav-menu {
|
569
|
+
display: flex;
|
570
|
+
}
|
571
|
+
|
572
|
+
/* Action buttons */
|
573
|
+
.dashboard-actions {
|
574
|
+
display: flex;
|
575
|
+
gap: 0.5rem;
|
576
|
+
}
|
577
|
+
|
578
|
+
.action-button {
|
579
|
+
display: inline-flex;
|
580
|
+
align-items: center;
|
581
|
+
padding: 0.5rem 1rem;
|
582
|
+
background-color: #f8f9fa;
|
583
|
+
border: 1px solid #dee2e6;
|
584
|
+
color: #333;
|
585
|
+
text-decoration: none;
|
586
|
+
border-radius: 4px;
|
587
|
+
font-size: 0.9rem;
|
588
|
+
cursor: pointer;
|
589
|
+
transition: all 0.2s;
|
590
|
+
}
|
591
|
+
|
592
|
+
.action-button:hover {
|
593
|
+
background-color: #e9ecef;
|
594
|
+
}
|
595
|
+
|
596
|
+
.action-icon {
|
597
|
+
margin-right: 0.25rem;
|
598
|
+
}
|
599
|
+
|
82
600
|
/* Fix for audit card to span full width when details are shown */
|
83
601
|
.audit-card {
|
84
602
|
position: relative;
|
85
603
|
transition: all 0.3s ease;
|
86
604
|
}
|
87
605
|
|
88
|
-
|
89
|
-
grid-column: 1 / -1;
|
90
|
-
width: 100%;
|
91
|
-
}
|
92
|
-
|
93
|
-
/* Card styles */
|
606
|
+
/* Card styles - simplified for tabs */
|
94
607
|
.stat-card {
|
95
|
-
background:
|
96
|
-
|
97
|
-
box-shadow:
|
98
|
-
padding: 1.5rem;
|
608
|
+
background: transparent;
|
609
|
+
padding: 0;
|
610
|
+
box-shadow: none;
|
99
611
|
overflow: visible;
|
100
612
|
}
|
101
613
|
|
@@ -108,82 +620,431 @@
|
|
108
620
|
gap: 0.5rem;
|
109
621
|
}
|
110
622
|
|
111
|
-
|
112
|
-
|
623
|
+
/* Security section specific styles */
|
624
|
+
.security-overview {
|
625
|
+
margin-bottom: 1.5rem;
|
626
|
+
background-color: #fff;
|
627
|
+
border-radius: 8px;
|
628
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
629
|
+
padding: 1.5rem;
|
113
630
|
}
|
114
631
|
|
115
|
-
|
116
|
-
.metric {
|
632
|
+
.security-score-container {
|
117
633
|
display: flex;
|
118
|
-
justify-content: space-between;
|
119
634
|
align-items: center;
|
120
|
-
|
635
|
+
gap: 2rem;
|
636
|
+
}
|
637
|
+
|
638
|
+
.security-score {
|
639
|
+
width: 120px;
|
640
|
+
height: 120px;
|
641
|
+
border-radius: 50%;
|
642
|
+
display: flex;
|
643
|
+
flex-direction: column;
|
644
|
+
align-items: center;
|
645
|
+
justify-content: center;
|
646
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
647
|
+
}
|
648
|
+
|
649
|
+
.score-value {
|
650
|
+
font-size: 3rem;
|
651
|
+
font-weight: 700;
|
652
|
+
line-height: 1;
|
653
|
+
}
|
654
|
+
|
655
|
+
.score-label {
|
656
|
+
font-size: 0.85rem;
|
657
|
+
text-align: center;
|
658
|
+
margin-top: 0.25rem;
|
659
|
+
}
|
660
|
+
|
661
|
+
.score-excellent {
|
662
|
+
background-color: #e9f7ef;
|
663
|
+
color: #27ae60;
|
664
|
+
border: 3px solid #27ae60;
|
665
|
+
}
|
666
|
+
|
667
|
+
.score-warning {
|
668
|
+
background-color: #fcf3cf;
|
669
|
+
color: #f39c12;
|
670
|
+
border: 3px solid #f39c12;
|
671
|
+
}
|
672
|
+
|
673
|
+
.score-critical {
|
674
|
+
background-color: #fdedeb;
|
675
|
+
color: #e74c3c;
|
676
|
+
border: 3px solid #e74c3c;
|
677
|
+
}
|
678
|
+
|
679
|
+
.security-metrics {
|
680
|
+
display: flex;
|
681
|
+
flex-grow: 1;
|
682
|
+
gap: 2rem;
|
683
|
+
}
|
684
|
+
|
685
|
+
.metric-item {
|
686
|
+
flex: 1;
|
687
|
+
display: flex;
|
688
|
+
align-items: center;
|
689
|
+
gap: 1rem;
|
690
|
+
padding: 1rem;
|
691
|
+
background-color: #f9fafb;
|
692
|
+
border-radius: 8px;
|
693
|
+
transition: transform 0.2s;
|
694
|
+
}
|
695
|
+
|
696
|
+
.metric-item:hover {
|
697
|
+
transform: translateY(-2px);
|
698
|
+
}
|
699
|
+
|
700
|
+
.metric-critical {
|
701
|
+
border-left: 4px solid #e74c3c;
|
702
|
+
}
|
703
|
+
|
704
|
+
.metric-warning {
|
705
|
+
border-left: 4px solid #f39c12;
|
706
|
+
}
|
707
|
+
|
708
|
+
.metric-icon {
|
709
|
+
font-size: 1.8rem;
|
710
|
+
opacity: 0.8;
|
711
|
+
}
|
712
|
+
|
713
|
+
.metric-data {
|
714
|
+
flex-grow: 1;
|
715
|
+
}
|
716
|
+
|
717
|
+
.metric-value {
|
718
|
+
font-size: 1.8rem;
|
719
|
+
font-weight: 700;
|
720
|
+
line-height: 1;
|
121
721
|
}
|
122
722
|
|
123
723
|
.metric-label {
|
724
|
+
font-size: 0.85rem;
|
124
725
|
color: #666;
|
125
|
-
|
726
|
+
margin-top: 0.25rem;
|
126
727
|
}
|
127
728
|
|
128
|
-
.
|
729
|
+
.security-tabs .tab-button {
|
730
|
+
position: relative;
|
731
|
+
}
|
732
|
+
|
733
|
+
.security-tabs .tab-button .tab-icon {
|
734
|
+
margin-right: 0.5rem;
|
735
|
+
}
|
736
|
+
|
737
|
+
.gem-impact-analysis {
|
738
|
+
padding: 1rem 0;
|
739
|
+
}
|
740
|
+
|
741
|
+
.gems-container {
|
742
|
+
display: grid;
|
743
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
744
|
+
gap: 1rem;
|
745
|
+
margin-top: 1.5rem;
|
746
|
+
}
|
747
|
+
|
748
|
+
.gem-card {
|
749
|
+
background-color: #fff;
|
750
|
+
border-radius: 8px;
|
751
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
752
|
+
overflow: hidden;
|
753
|
+
transition: transform 0.2s;
|
754
|
+
}
|
755
|
+
|
756
|
+
.gem-card:hover {
|
757
|
+
transform: translateY(-2px);
|
758
|
+
}
|
759
|
+
|
760
|
+
.gem-card.severity-critical {
|
761
|
+
border-top: 4px solid #e74c3c;
|
762
|
+
}
|
763
|
+
|
764
|
+
.gem-card.severity-high {
|
765
|
+
border-top: 4px solid #fd7e14;
|
766
|
+
}
|
767
|
+
|
768
|
+
.gem-card.severity-medium {
|
769
|
+
border-top: 4px solid #f39c12;
|
770
|
+
}
|
771
|
+
|
772
|
+
.gem-card.severity-low {
|
773
|
+
border-top: 4px solid #27ae60;
|
774
|
+
}
|
775
|
+
|
776
|
+
.gem-header {
|
777
|
+
padding: 1rem;
|
778
|
+
display: flex;
|
779
|
+
justify-content: space-between;
|
780
|
+
align-items: center;
|
781
|
+
border-bottom: 1px solid #eeeeee;
|
782
|
+
}
|
783
|
+
|
784
|
+
.gem-name {
|
129
785
|
font-weight: 600;
|
130
|
-
|
786
|
+
color: #333;
|
131
787
|
}
|
132
788
|
|
133
|
-
|
134
|
-
.status-badge {
|
135
|
-
display: inline-block;
|
136
|
-
padding: 0.35rem 0.75rem;
|
137
|
-
border-radius: 20px;
|
789
|
+
.gem-severity {
|
138
790
|
font-size: 0.85rem;
|
139
|
-
|
140
|
-
|
791
|
+
padding: 0.2rem 0.75rem;
|
792
|
+
border-radius: 12px;
|
141
793
|
}
|
142
794
|
|
143
|
-
.
|
144
|
-
background-color:
|
145
|
-
color: #
|
795
|
+
.gem-severity.critical {
|
796
|
+
background-color: rgba(231, 76, 60, 0.15);
|
797
|
+
color: #e74c3c;
|
146
798
|
}
|
147
799
|
|
148
|
-
.
|
149
|
-
background-color:
|
150
|
-
color: #
|
800
|
+
.gem-severity.high {
|
801
|
+
background-color: rgba(253, 126, 20, 0.15);
|
802
|
+
color: #fd7e14;
|
151
803
|
}
|
152
804
|
|
153
|
-
.
|
154
|
-
background-color:
|
155
|
-
color: #
|
805
|
+
.gem-severity.medium {
|
806
|
+
background-color: rgba(243, 156, 18, 0.15);
|
807
|
+
color: #f39c12;
|
156
808
|
}
|
157
809
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
810
|
+
.gem-severity.low {
|
811
|
+
background-color: rgba(39, 174, 96, 0.15);
|
812
|
+
color: #27ae60;
|
813
|
+
}
|
814
|
+
|
815
|
+
.gem-severity.unknown {
|
816
|
+
background-color: rgba(173, 181, 189, 0.15);
|
817
|
+
color: #6c757d;
|
818
|
+
}
|
819
|
+
|
820
|
+
.gem-details {
|
821
|
+
padding: 1rem;
|
822
|
+
background-color: #f9fafb;
|
823
|
+
}
|
824
|
+
|
825
|
+
.gem-versions {
|
826
|
+
display: flex;
|
827
|
+
justify-content: space-between;
|
165
828
|
margin-bottom: 0.75rem;
|
829
|
+
font-size: 0.9rem;
|
166
830
|
}
|
167
831
|
|
168
|
-
.
|
169
|
-
|
170
|
-
|
832
|
+
.version-label {
|
833
|
+
color: #666;
|
834
|
+
}
|
835
|
+
|
836
|
+
.version-value {
|
837
|
+
font-family: monospace;
|
838
|
+
}
|
839
|
+
|
840
|
+
.gem-vulnerabilities-count {
|
841
|
+
font-size: 0.85rem;
|
842
|
+
color: #666;
|
843
|
+
}
|
844
|
+
|
845
|
+
.gem-actions {
|
846
|
+
padding: 1rem;
|
847
|
+
display: flex;
|
848
|
+
justify-content: center;
|
849
|
+
}
|
850
|
+
|
851
|
+
.empty-state {
|
852
|
+
text-align: center;
|
853
|
+
padding: 3rem 1rem;
|
854
|
+
}
|
855
|
+
|
856
|
+
.empty-icon {
|
857
|
+
font-size: 3rem;
|
858
|
+
margin-bottom: 1rem;
|
859
|
+
color: #27ae60;
|
860
|
+
}
|
861
|
+
|
862
|
+
.empty-message {
|
863
|
+
font-size: 1.2rem;
|
864
|
+
font-weight: 600;
|
865
|
+
margin-bottom: 0.5rem;
|
866
|
+
}
|
867
|
+
|
868
|
+
.empty-description {
|
869
|
+
color: #666;
|
870
|
+
}
|
871
|
+
|
872
|
+
.security-timeline-container {
|
873
|
+
padding: 1rem 0;
|
874
|
+
}
|
875
|
+
|
876
|
+
.timeline-chart-placeholder {
|
877
|
+
background-color: #fff;
|
878
|
+
border-radius: 8px;
|
879
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
880
|
+
padding: 1.5rem;
|
881
|
+
margin-top: 1.5rem;
|
882
|
+
}
|
883
|
+
|
884
|
+
.chart-header {
|
885
|
+
display: flex;
|
886
|
+
justify-content: space-between;
|
887
|
+
align-items: center;
|
888
|
+
margin-bottom: 1.5rem;
|
889
|
+
}
|
890
|
+
|
891
|
+
.chart-title {
|
892
|
+
font-weight: 600;
|
893
|
+
font-size: 1.1rem;
|
894
|
+
}
|
895
|
+
|
896
|
+
.chart-legend {
|
897
|
+
display: flex;
|
898
|
+
gap: 1rem;
|
899
|
+
}
|
900
|
+
|
901
|
+
.legend-item {
|
902
|
+
display: flex;
|
903
|
+
align-items: center;
|
904
|
+
gap: 0.5rem;
|
905
|
+
font-size: 0.85rem;
|
906
|
+
}
|
907
|
+
|
908
|
+
.legend-color {
|
909
|
+
width: 12px;
|
910
|
+
height: 12px;
|
911
|
+
border-radius: 2px;
|
912
|
+
}
|
913
|
+
|
914
|
+
.chart-visualization {
|
915
|
+
height: 200px;
|
916
|
+
position: relative;
|
917
|
+
margin-bottom: 1.5rem;
|
918
|
+
}
|
919
|
+
|
920
|
+
.chart-timeline {
|
921
|
+
height: 80px;
|
922
|
+
background-color: #f8f9fa;
|
171
923
|
border-radius: 4px;
|
924
|
+
position: relative;
|
925
|
+
margin-top: 40px;
|
172
926
|
}
|
173
927
|
|
174
|
-
|
175
|
-
|
176
|
-
|
928
|
+
.timeline-point {
|
929
|
+
position: absolute;
|
930
|
+
bottom: 0;
|
931
|
+
transform: translateX(-50%);
|
177
932
|
}
|
178
933
|
|
179
|
-
.
|
180
|
-
|
934
|
+
.timeline-marker {
|
935
|
+
width: 12px;
|
936
|
+
height: 12px;
|
937
|
+
border-radius: 50%;
|
938
|
+
position: relative;
|
181
939
|
}
|
182
940
|
|
183
|
-
.
|
941
|
+
.timeline-marker::before {
|
942
|
+
content: '';
|
943
|
+
position: absolute;
|
944
|
+
bottom: 100%;
|
945
|
+
left: 50%;
|
946
|
+
transform: translateX(-50%);
|
947
|
+
width: 1px;
|
948
|
+
height: 30px;
|
949
|
+
background-color: rgba(0,0,0,0.1);
|
950
|
+
}
|
951
|
+
|
952
|
+
.timeline-marker.critical {
|
184
953
|
background-color: #dc3545;
|
185
954
|
}
|
186
955
|
|
956
|
+
.timeline-marker.medium {
|
957
|
+
background-color: #ffc107;
|
958
|
+
}
|
959
|
+
|
960
|
+
.timeline-marker.low {
|
961
|
+
background-color: #28a745;
|
962
|
+
}
|
963
|
+
|
964
|
+
.timeline-date {
|
965
|
+
position: absolute;
|
966
|
+
bottom: 100%;
|
967
|
+
left: 50%;
|
968
|
+
transform: translateX(-50%);
|
969
|
+
white-space: nowrap;
|
970
|
+
font-size: 0.85rem;
|
971
|
+
color: #666;
|
972
|
+
margin-bottom: 0.5rem;
|
973
|
+
}
|
974
|
+
|
975
|
+
.timeline-insights {
|
976
|
+
margin-top: 1.5rem;
|
977
|
+
}
|
978
|
+
|
979
|
+
.insight-card {
|
980
|
+
background-color: #fff;
|
981
|
+
border-radius: 8px;
|
982
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
983
|
+
overflow: hidden;
|
984
|
+
}
|
985
|
+
|
986
|
+
.insight-header {
|
987
|
+
background-color: #f8f9fa;
|
988
|
+
padding: 1rem;
|
989
|
+
font-weight: 600;
|
990
|
+
border-bottom: 1px solid #eeeeee;
|
991
|
+
}
|
992
|
+
|
993
|
+
.insight-content {
|
994
|
+
padding: 1rem;
|
995
|
+
}
|
996
|
+
|
997
|
+
.insight-item {
|
998
|
+
margin-bottom: 1rem;
|
999
|
+
}
|
1000
|
+
|
1001
|
+
.insight-item:last-child {
|
1002
|
+
margin-bottom: 0;
|
1003
|
+
}
|
1004
|
+
|
1005
|
+
.insight-title {
|
1006
|
+
font-weight: 600;
|
1007
|
+
font-size: 0.95rem;
|
1008
|
+
margin-bottom: 0.5rem;
|
1009
|
+
}
|
1010
|
+
|
1011
|
+
.insight-description {
|
1012
|
+
color: #555;
|
1013
|
+
font-size: 0.9rem;
|
1014
|
+
}
|
1015
|
+
|
1016
|
+
/* Responsive adjustments */
|
1017
|
+
@media (max-width: 768px) {
|
1018
|
+
.dashboard-nav {
|
1019
|
+
flex-direction: column;
|
1020
|
+
gap: 1rem;
|
1021
|
+
}
|
1022
|
+
|
1023
|
+
.dashboard-nav ul {
|
1024
|
+
width: 100%;
|
1025
|
+
overflow-x: auto;
|
1026
|
+
padding-bottom: 0.5rem;
|
1027
|
+
}
|
1028
|
+
|
1029
|
+
.dashboard-actions {
|
1030
|
+
width: 100%;
|
1031
|
+
}
|
1032
|
+
|
1033
|
+
.action-button {
|
1034
|
+
flex: 1;
|
1035
|
+
justify-content: center;
|
1036
|
+
}
|
1037
|
+
|
1038
|
+
.tabs-header {
|
1039
|
+
padding: 0;
|
1040
|
+
}
|
1041
|
+
|
1042
|
+
.tab-button {
|
1043
|
+
padding: 0.75rem 1rem;
|
1044
|
+
}
|
1045
|
+
}
|
1046
|
+
|
1047
|
+
/* Keep compatibility with existing styles */
|
187
1048
|
/* Details panel styling */
|
188
1049
|
.details-panel {
|
189
1050
|
background: #f9f9f9;
|
@@ -215,102 +1076,96 @@
|
|
215
1076
|
text-decoration: underline;
|
216
1077
|
}
|
217
1078
|
|
218
|
-
/*
|
219
|
-
.
|
220
|
-
|
221
|
-
margin-top: 1rem;
|
222
|
-
}
|
223
|
-
|
224
|
-
.table {
|
225
|
-
width: 100%;
|
226
|
-
border-collapse: collapse;
|
227
|
-
font-size: 0.9rem;
|
1079
|
+
/* Status colors */
|
1080
|
+
.status-ok {
|
1081
|
+
border-left: 4px solid #28a745;
|
228
1082
|
}
|
229
1083
|
|
230
|
-
.
|
231
|
-
|
232
|
-
padding: 0.75rem;
|
233
|
-
background-color: #f8f9fa;
|
234
|
-
border-bottom: 2px solid #dee2e6;
|
1084
|
+
.status-warning {
|
1085
|
+
border-left: 4px solid #ffc107;
|
235
1086
|
}
|
236
1087
|
|
237
|
-
.
|
238
|
-
|
239
|
-
border-bottom: 1px solid #dee2e6;
|
240
|
-
vertical-align: middle;
|
1088
|
+
.status-danger {
|
1089
|
+
border-left: 4px solid #dc3545;
|
241
1090
|
}
|
242
1091
|
|
243
|
-
/*
|
244
|
-
.
|
245
|
-
|
246
|
-
|
1092
|
+
/* Toast notifications styling */
|
1093
|
+
.toast-notification {
|
1094
|
+
position: fixed;
|
1095
|
+
bottom: 20px;
|
1096
|
+
left: 50%;
|
1097
|
+
transform: translateX(-50%) translateY(100%);
|
1098
|
+
background-color: white;
|
1099
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
247
1100
|
border-radius: 4px;
|
248
|
-
|
249
|
-
font-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
background-color: #f8d7da;
|
254
|
-
color: #721c24;
|
255
|
-
}
|
256
|
-
|
257
|
-
.severity.medium {
|
258
|
-
background-color: #fff3cd;
|
259
|
-
color: #856404;
|
260
|
-
}
|
261
|
-
|
262
|
-
.severity.low {
|
263
|
-
background-color: #d1ecf1;
|
264
|
-
color: #0c5460;
|
1101
|
+
padding: 0.75rem 1.5rem;
|
1102
|
+
font-size: 0.9rem;
|
1103
|
+
z-index: 1000;
|
1104
|
+
transition: transform 0.3s ease;
|
1105
|
+
opacity: 0;
|
265
1106
|
}
|
266
1107
|
|
267
|
-
.
|
268
|
-
|
269
|
-
|
1108
|
+
.toast-notification.visible {
|
1109
|
+
transform: translateX(-50%) translateY(0);
|
1110
|
+
opacity: 1;
|
270
1111
|
}
|
271
1112
|
|
272
|
-
|
273
|
-
|
274
|
-
display: flex;
|
275
|
-
gap: 1rem;
|
276
|
-
margin-top: 2rem;
|
1113
|
+
.toast-notification.success {
|
1114
|
+
border-left: 4px solid #28a745;
|
277
1115
|
}
|
278
1116
|
|
279
|
-
.
|
280
|
-
|
281
|
-
padding: 0.5rem 1rem;
|
282
|
-
background-color: #007bff;
|
283
|
-
color: white;
|
284
|
-
text-decoration: none;
|
285
|
-
border-radius: 4px;
|
286
|
-
font-size: 0.9rem;
|
287
|
-
cursor: pointer;
|
288
|
-
transition: background-color 0.2s;
|
1117
|
+
.toast-notification.info {
|
1118
|
+
border-left: 4px solid #17a2b8;
|
289
1119
|
}
|
290
1120
|
|
291
|
-
.
|
292
|
-
|
1121
|
+
.toast-notification.warning {
|
1122
|
+
border-left: 4px solid #ffc107;
|
293
1123
|
}
|
294
1124
|
|
295
|
-
|
296
|
-
|
297
|
-
.stats-cards {
|
298
|
-
grid-template-columns: 1fr;
|
299
|
-
}
|
300
|
-
|
301
|
-
.dashboard-actions {
|
302
|
-
flex-direction: column;
|
303
|
-
}
|
304
|
-
|
305
|
-
.action-button {
|
306
|
-
width: 100%;
|
307
|
-
text-align: center;
|
308
|
-
}
|
1125
|
+
.toast-notification.error {
|
1126
|
+
border-left: 4px solid #dc3545;
|
309
1127
|
}
|
310
1128
|
</style>
|
311
1129
|
|
312
1130
|
<script>
|
313
1131
|
document.addEventListener('DOMContentLoaded', function() {
|
1132
|
+
// Check for hash in URL and navigate accordingly
|
1133
|
+
function handleHashNavigation() {
|
1134
|
+
if (location.hash) {
|
1135
|
+
const hashParts = location.hash.substring(1).split('/');
|
1136
|
+
const section = hashParts[0];
|
1137
|
+
const tab = hashParts[1] || null;
|
1138
|
+
|
1139
|
+
if (section && document.getElementById(section)) {
|
1140
|
+
// On page load, don't scroll - this prevents jumping around
|
1141
|
+
// For manual hash changes, do scroll
|
1142
|
+
const isPageLoad = !window.hasPageLoaded;
|
1143
|
+
navigateToSection(section, tab, !isPageLoad);
|
1144
|
+
return true;
|
1145
|
+
}
|
1146
|
+
}
|
1147
|
+
return false;
|
1148
|
+
}
|
1149
|
+
|
1150
|
+
// Track if page has loaded fully
|
1151
|
+
window.hasPageLoaded = false;
|
1152
|
+
|
1153
|
+
// Try to handle hash navigation, if it fails (no hash or invalid section),
|
1154
|
+
// the default 'overview' section will remain active
|
1155
|
+
const hashNavigated = handleHashNavigation();
|
1156
|
+
if (!hashNavigated) {
|
1157
|
+
// If no hash navigation happened, set default hash to #overview
|
1158
|
+
updateUrlHash('overview');
|
1159
|
+
}
|
1160
|
+
|
1161
|
+
// Listen for hash changes
|
1162
|
+
window.addEventListener('hashchange', handleHashNavigation);
|
1163
|
+
|
1164
|
+
// Mark page as fully loaded after a short delay
|
1165
|
+
setTimeout(() => {
|
1166
|
+
window.hasPageLoaded = true;
|
1167
|
+
}, 500);
|
1168
|
+
|
314
1169
|
// Toggle details panels
|
315
1170
|
document.querySelectorAll('.toggle-details').forEach(function(toggle) {
|
316
1171
|
toggle.addEventListener('click', function(e) {
|
@@ -332,16 +1187,165 @@
|
|
332
1187
|
}
|
333
1188
|
});
|
334
1189
|
});
|
1190
|
+
|
1191
|
+
// Function to update URL hash with current state
|
1192
|
+
function updateUrlHash(section, tab = null) {
|
1193
|
+
let hash = '#' + section;
|
1194
|
+
if (tab) {
|
1195
|
+
hash += '/' + tab;
|
1196
|
+
}
|
1197
|
+
history.replaceState(null, null, hash);
|
1198
|
+
}
|
1199
|
+
|
1200
|
+
// Function to navigate to section and tab
|
1201
|
+
function navigateToSection(section, tab = null, shouldScroll = false) {
|
1202
|
+
// Remove active class from all nav items and sections
|
1203
|
+
document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
|
1204
|
+
document.querySelectorAll('.dashboard-section').forEach(section => section.classList.remove('active'));
|
1205
|
+
|
1206
|
+
// Add active class to matching nav item
|
1207
|
+
const navItem = document.querySelector(`.nav-item[data-section="${section}"]`);
|
1208
|
+
if (navItem) {
|
1209
|
+
navItem.classList.add('active');
|
1210
|
+
}
|
1211
|
+
|
1212
|
+
// Show corresponding section
|
1213
|
+
const sectionElement = document.getElementById(section);
|
1214
|
+
if (sectionElement) {
|
1215
|
+
sectionElement.classList.add('active');
|
1216
|
+
|
1217
|
+
// If tab is specified, activate that tab
|
1218
|
+
if (tab) {
|
1219
|
+
const tabsContainer = sectionElement.querySelector('.tabs-container');
|
1220
|
+
if (tabsContainer) {
|
1221
|
+
// Deactivate all tabs first
|
1222
|
+
tabsContainer.querySelectorAll('.tab-button').forEach(button => button.classList.remove('active'));
|
1223
|
+
tabsContainer.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
1224
|
+
|
1225
|
+
// Activate the target tab
|
1226
|
+
const targetTabButton = tabsContainer.querySelector(`.tab-button[data-tab="${tab}"]`);
|
1227
|
+
const targetTabContent = tabsContainer.querySelector(`#${tab}`);
|
1228
|
+
|
1229
|
+
if (targetTabButton) targetTabButton.classList.add('active');
|
1230
|
+
if (targetTabContent) targetTabContent.classList.add('active');
|
1231
|
+
}
|
1232
|
+
}
|
1233
|
+
|
1234
|
+
// Scroll to section if requested (with a small delay to ensure rendering)
|
1235
|
+
if (shouldScroll) {
|
1236
|
+
setTimeout(() => {
|
1237
|
+
sectionElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
1238
|
+
}, 100);
|
1239
|
+
}
|
1240
|
+
|
1241
|
+
// Update URL hash
|
1242
|
+
updateUrlHash(section, tab);
|
1243
|
+
}
|
1244
|
+
}
|
1245
|
+
|
1246
|
+
// Main navigation
|
1247
|
+
document.querySelectorAll('.nav-item').forEach(function(navItem) {
|
1248
|
+
navItem.addEventListener('click', function(e) {
|
1249
|
+
e.preventDefault();
|
1250
|
+
|
1251
|
+
// Get section ID from data attribute
|
1252
|
+
const sectionId = this.getAttribute('data-section');
|
1253
|
+
|
1254
|
+
// Navigate to the section
|
1255
|
+
navigateToSection(sectionId);
|
1256
|
+
});
|
1257
|
+
});
|
1258
|
+
|
1259
|
+
// Tab navigation
|
1260
|
+
document.querySelectorAll('.tab-button').forEach(function(tabButton) {
|
1261
|
+
tabButton.addEventListener('click', function() {
|
1262
|
+
const tabContainer = this.closest('.tabs-container');
|
1263
|
+
const tabId = this.getAttribute('data-tab');
|
1264
|
+
|
1265
|
+
// Find the current active section
|
1266
|
+
const currentSection = document.querySelector('.dashboard-section.active').id;
|
1267
|
+
|
1268
|
+
// Remove active class from all buttons and tabs within this container
|
1269
|
+
tabContainer.querySelectorAll('.tab-button').forEach(button => button.classList.remove('active'));
|
1270
|
+
tabContainer.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
1271
|
+
|
1272
|
+
// Add active class to clicked button and corresponding tab
|
1273
|
+
this.classList.add('active');
|
1274
|
+
tabContainer.querySelector(`#${tabId}`).classList.add('active');
|
1275
|
+
|
1276
|
+
// Update URL hash with current section and tab
|
1277
|
+
updateUrlHash(currentSection, tabId);
|
1278
|
+
});
|
1279
|
+
});
|
1280
|
+
|
1281
|
+
// Quick navigation
|
1282
|
+
document.querySelectorAll('.quick-nav-item').forEach(function(navItem) {
|
1283
|
+
navItem.addEventListener('click', function(e) {
|
1284
|
+
e.preventDefault();
|
1285
|
+
const targetId = this.getAttribute('href').substring(1);
|
1286
|
+
|
1287
|
+
// Navigate to the specified section
|
1288
|
+
navigateToSection(targetId);
|
1289
|
+
|
1290
|
+
// Close the quick nav menu
|
1291
|
+
document.querySelector('.quick-nav-menu').style.display = 'none';
|
1292
|
+
});
|
1293
|
+
});
|
1294
|
+
|
1295
|
+
// Summary card navigation
|
1296
|
+
document.querySelectorAll('.summary-card').forEach(function(card) {
|
1297
|
+
card.addEventListener('click', function() {
|
1298
|
+
const section = this.getAttribute('data-section');
|
1299
|
+
const tab = this.getAttribute('data-tab');
|
1300
|
+
|
1301
|
+
// Navigate to the specified section and tab, with scrolling
|
1302
|
+
navigateToSection(section, tab, true);
|
1303
|
+
});
|
1304
|
+
});
|
335
1305
|
});
|
336
1306
|
|
337
1307
|
function refreshAudit() {
|
338
|
-
//
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
//
|
345
|
-
|
1308
|
+
// Show loading indicator
|
1309
|
+
const refreshButton = document.querySelector('.action-button');
|
1310
|
+
const originalText = refreshButton.innerHTML;
|
1311
|
+
refreshButton.innerHTML = '<span class="action-icon">โณ</span> Refreshing...';
|
1312
|
+
refreshButton.disabled = true;
|
1313
|
+
|
1314
|
+
// Make AJAX call to refresh endpoint
|
1315
|
+
fetch('/solidstats/refresh', {
|
1316
|
+
method: 'GET',
|
1317
|
+
headers: {
|
1318
|
+
'Accept': 'application/json',
|
1319
|
+
'X-Requested-With': 'XMLHttpRequest'
|
1320
|
+
},
|
1321
|
+
credentials: 'same-origin'
|
1322
|
+
})
|
1323
|
+
.then(response => {
|
1324
|
+
if (!response.ok) {
|
1325
|
+
throw new Error('Network response was not ok');
|
1326
|
+
}
|
1327
|
+
return response.json();
|
1328
|
+
})
|
1329
|
+
.then(data => {
|
1330
|
+
// Update the dashboard with fresh data
|
1331
|
+
location.reload();
|
1332
|
+
|
1333
|
+
// Show success notification
|
1334
|
+
showNotification('Dashboard data refreshed successfully', 'success');
|
1335
|
+
|
1336
|
+
// Reset button state
|
1337
|
+
refreshButton.innerHTML = originalText;
|
1338
|
+
refreshButton.disabled = false;
|
1339
|
+
})
|
1340
|
+
.catch(error => {
|
1341
|
+
console.error('Error refreshing data:', error);
|
1342
|
+
|
1343
|
+
// Show error notification
|
1344
|
+
showNotification('Failed to refresh data. Please try again.', 'error');
|
1345
|
+
|
1346
|
+
// Reset button state
|
1347
|
+
refreshButton.innerHTML = originalText;
|
1348
|
+
refreshButton.disabled = false;
|
1349
|
+
});
|
346
1350
|
}
|
347
1351
|
</script>
|