@ipation/specbridge 1.2.0 → 1.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.
- package/CHANGELOG.md +14 -1
- package/README.md +32 -1
- package/dist/public/index.html +551 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [1.2.
|
|
10
|
+
## [1.2.1] - 2026-02-03
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Dashboard Chart.js rendering**: Fixed chart initialization timing issue by adding 100ms delay to ensure canvas element is fully mounted before rendering
|
|
14
|
+
- **Build process**: Configured tsup to automatically copy dashboard static files (`src/dashboard/public/`) to `dist/public/` during build
|
|
15
|
+
- **Chart enhancements**: Added better error handling, formatted date labels, tooltips, and improved visual styling for compliance trend chart
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **Build output**: Dashboard static files now automatically included in build output without manual intervention
|
|
19
|
+
- **.gitignore**: Added `.playwright-mcp/` and `.specbridge/reports/history/` to ignore test and runtime artifacts
|
|
20
|
+
|
|
21
|
+
## [1.2.0] - 2026-02-03
|
|
22
|
+
|
|
23
|
+
### Phase 4: Analytics & Insights
|
|
11
24
|
|
|
12
25
|
### Added
|
|
13
26
|
|
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ SpecBridge creates a living integration layer between design intent and implemen
|
|
|
16
16
|
- **Verification Engine** - Continuously verifies code compliance at multiple levels
|
|
17
17
|
- **Propagation Engine** - Analyzes impact when architectural decisions change
|
|
18
18
|
- **Compliance Reporting** - Provides dashboards and tracks conformity over time
|
|
19
|
+
- **Analytics & Insights** - AI-generated insights, drift detection, and trend analysis
|
|
20
|
+
- **Interactive Dashboard** - Real-time compliance monitoring with visual charts
|
|
19
21
|
- **Agent Interface** - Exposes decisions to code generation agents (Copilot, Claude, etc.)
|
|
20
22
|
|
|
21
23
|
## Installation
|
|
@@ -92,7 +94,31 @@ specbridge report
|
|
|
92
94
|
specbridge report --format markdown --save
|
|
93
95
|
```
|
|
94
96
|
|
|
95
|
-
|
|
97
|
+
Track compliance trends over time:
|
|
98
|
+
```bash
|
|
99
|
+
specbridge report --trend --days 30
|
|
100
|
+
specbridge report --drift
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 6. Analyze compliance with AI insights
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
specbridge analytics
|
|
107
|
+
specbridge analytics --insights
|
|
108
|
+
specbridge analytics auth-001
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Get AI-generated insights about compliance trends, violations, and decision impact.
|
|
112
|
+
|
|
113
|
+
### 7. Launch interactive dashboard
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
specbridge dashboard
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Open your browser to view real-time compliance metrics, trend charts, and insights.
|
|
120
|
+
|
|
121
|
+
### 8. Integrate with AI agents
|
|
96
122
|
|
|
97
123
|
```bash
|
|
98
124
|
specbridge context src/api/auth.ts
|
|
@@ -222,6 +248,11 @@ verification:
|
|
|
222
248
|
| `specbridge decision create <id>` | Create new decision |
|
|
223
249
|
| `specbridge decision validate` | Validate decision files |
|
|
224
250
|
| `specbridge report` | Generate compliance report |
|
|
251
|
+
| `specbridge report --trend` | Show compliance trends over time |
|
|
252
|
+
| `specbridge report --drift` | Analyze drift since last report |
|
|
253
|
+
| `specbridge analytics` | Analyze compliance with AI insights |
|
|
254
|
+
| `specbridge analytics <id>` | Analyze specific decision |
|
|
255
|
+
| `specbridge dashboard` | Launch interactive web dashboard |
|
|
225
256
|
| `specbridge hook install` | Install git hooks |
|
|
226
257
|
| `specbridge hook run` | Run verification (for hooks) |
|
|
227
258
|
| `specbridge context <file>` | Generate agent context |
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>SpecBridge Compliance Dashboard</title>
|
|
7
|
+
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
8
|
+
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
9
|
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0"></script>
|
|
11
|
+
<style>
|
|
12
|
+
* {
|
|
13
|
+
margin: 0;
|
|
14
|
+
padding: 0;
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
|
20
|
+
-webkit-font-smoothing: antialiased;
|
|
21
|
+
-moz-osx-font-smoothing: grayscale;
|
|
22
|
+
background: #f5f7fa;
|
|
23
|
+
color: #2c3e50;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.header {
|
|
27
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
28
|
+
color: white;
|
|
29
|
+
padding: 2rem;
|
|
30
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.header h1 {
|
|
34
|
+
font-size: 2rem;
|
|
35
|
+
margin-bottom: 0.5rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.header p {
|
|
39
|
+
opacity: 0.9;
|
|
40
|
+
font-size: 1rem;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.container {
|
|
44
|
+
max-width: 1400px;
|
|
45
|
+
margin: 0 auto;
|
|
46
|
+
padding: 2rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.loading {
|
|
50
|
+
text-align: center;
|
|
51
|
+
padding: 4rem;
|
|
52
|
+
font-size: 1.2rem;
|
|
53
|
+
color: #666;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.error {
|
|
57
|
+
background: #fee;
|
|
58
|
+
border: 1px solid #fcc;
|
|
59
|
+
border-radius: 8px;
|
|
60
|
+
padding: 1rem;
|
|
61
|
+
margin: 1rem 0;
|
|
62
|
+
color: #c33;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.grid {
|
|
66
|
+
display: grid;
|
|
67
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
68
|
+
gap: 1.5rem;
|
|
69
|
+
margin-bottom: 2rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.card {
|
|
73
|
+
background: white;
|
|
74
|
+
border-radius: 12px;
|
|
75
|
+
padding: 1.5rem;
|
|
76
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
77
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.card:hover {
|
|
81
|
+
transform: translateY(-2px);
|
|
82
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.card h2 {
|
|
86
|
+
font-size: 1.5rem;
|
|
87
|
+
margin-bottom: 1rem;
|
|
88
|
+
color: #2c3e50;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.metric {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
margin-bottom: 0.75rem;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.metric-label {
|
|
99
|
+
font-size: 0.9rem;
|
|
100
|
+
color: #666;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.metric-value {
|
|
104
|
+
font-size: 1.5rem;
|
|
105
|
+
font-weight: bold;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.metric-value.success {
|
|
109
|
+
color: #27ae60;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.metric-value.warning {
|
|
113
|
+
color: #f39c12;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.metric-value.danger {
|
|
117
|
+
color: #e74c3c;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.compliance-score {
|
|
121
|
+
text-align: center;
|
|
122
|
+
padding: 2rem;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.compliance-score .score {
|
|
126
|
+
font-size: 4rem;
|
|
127
|
+
font-weight: bold;
|
|
128
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
129
|
+
-webkit-background-clip: text;
|
|
130
|
+
-webkit-text-fill-color: transparent;
|
|
131
|
+
background-clip: text;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.compliance-score .label {
|
|
135
|
+
font-size: 0.9rem;
|
|
136
|
+
color: #666;
|
|
137
|
+
text-transform: uppercase;
|
|
138
|
+
letter-spacing: 1px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.trend-indicator {
|
|
142
|
+
display: inline-flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 0.25rem;
|
|
145
|
+
padding: 0.25rem 0.75rem;
|
|
146
|
+
border-radius: 12px;
|
|
147
|
+
font-size: 0.85rem;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.trend-indicator.up {
|
|
152
|
+
background: #d4edda;
|
|
153
|
+
color: #155724;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.trend-indicator.down {
|
|
157
|
+
background: #f8d7da;
|
|
158
|
+
color: #721c24;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.trend-indicator.stable {
|
|
162
|
+
background: #fff3cd;
|
|
163
|
+
color: #856404;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.chart-container {
|
|
167
|
+
position: relative;
|
|
168
|
+
height: 300px;
|
|
169
|
+
margin-top: 1rem;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.decision-list {
|
|
173
|
+
list-style: none;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.decision-item {
|
|
177
|
+
padding: 0.75rem;
|
|
178
|
+
border-bottom: 1px solid #eee;
|
|
179
|
+
display: flex;
|
|
180
|
+
justify-content: space-between;
|
|
181
|
+
align-items: center;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.decision-item:last-child {
|
|
185
|
+
border-bottom: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.decision-name {
|
|
189
|
+
font-weight: 500;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.decision-compliance {
|
|
193
|
+
font-weight: bold;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.violation-badge {
|
|
197
|
+
display: inline-block;
|
|
198
|
+
padding: 0.25rem 0.5rem;
|
|
199
|
+
border-radius: 4px;
|
|
200
|
+
font-size: 0.75rem;
|
|
201
|
+
font-weight: 600;
|
|
202
|
+
text-transform: uppercase;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.violation-badge.critical {
|
|
206
|
+
background: #fee;
|
|
207
|
+
color: #c33;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.violation-badge.high {
|
|
211
|
+
background: #fff3cd;
|
|
212
|
+
color: #856404;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.violation-badge.medium {
|
|
216
|
+
background: #d1ecf1;
|
|
217
|
+
color: #0c5460;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.violation-badge.low {
|
|
221
|
+
background: #e2e3e5;
|
|
222
|
+
color: #383d41;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.insights {
|
|
226
|
+
margin-top: 1rem;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.insight {
|
|
230
|
+
padding: 0.75rem;
|
|
231
|
+
border-left: 4px solid #ccc;
|
|
232
|
+
margin-bottom: 0.75rem;
|
|
233
|
+
background: #f9f9f9;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.insight.warning {
|
|
237
|
+
border-color: #f39c12;
|
|
238
|
+
background: #fff9e6;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.insight.success {
|
|
242
|
+
border-color: #27ae60;
|
|
243
|
+
background: #e8f8f0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.insight.info {
|
|
247
|
+
border-color: #3498db;
|
|
248
|
+
background: #e8f4fc;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.insight-message {
|
|
252
|
+
font-weight: 500;
|
|
253
|
+
margin-bottom: 0.25rem;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.insight-details {
|
|
257
|
+
font-size: 0.85rem;
|
|
258
|
+
color: #666;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.timestamp {
|
|
262
|
+
text-align: center;
|
|
263
|
+
color: #999;
|
|
264
|
+
font-size: 0.85rem;
|
|
265
|
+
margin-top: 2rem;
|
|
266
|
+
padding: 1rem;
|
|
267
|
+
}
|
|
268
|
+
</style>
|
|
269
|
+
</head>
|
|
270
|
+
<body>
|
|
271
|
+
<div id="root"></div>
|
|
272
|
+
|
|
273
|
+
<script type="text/babel">
|
|
274
|
+
const { useState, useEffect } = React;
|
|
275
|
+
|
|
276
|
+
function Dashboard() {
|
|
277
|
+
const [loading, setLoading] = useState(true);
|
|
278
|
+
const [error, setError] = useState(null);
|
|
279
|
+
const [report, setReport] = useState(null);
|
|
280
|
+
const [history, setHistory] = useState([]);
|
|
281
|
+
const [analytics, setAnalytics] = useState(null);
|
|
282
|
+
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
loadData();
|
|
285
|
+
}, []);
|
|
286
|
+
|
|
287
|
+
async function loadData() {
|
|
288
|
+
try {
|
|
289
|
+
setLoading(true);
|
|
290
|
+
setError(null);
|
|
291
|
+
|
|
292
|
+
// Load latest report
|
|
293
|
+
const reportRes = await fetch('/api/report/latest');
|
|
294
|
+
if (!reportRes.ok) throw new Error('Failed to load report');
|
|
295
|
+
const reportData = await reportRes.json();
|
|
296
|
+
setReport(reportData);
|
|
297
|
+
|
|
298
|
+
// Load history for chart
|
|
299
|
+
const historyRes = await fetch('/api/report/history?days=30');
|
|
300
|
+
if (historyRes.ok) {
|
|
301
|
+
const historyData = await historyRes.json();
|
|
302
|
+
setHistory(historyData);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Load analytics
|
|
306
|
+
const analyticsRes = await fetch('/api/analytics/summary?days=90');
|
|
307
|
+
if (analyticsRes.ok) {
|
|
308
|
+
const analyticsData = await analyticsRes.json();
|
|
309
|
+
setAnalytics(analyticsData);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
setLoading(false);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
setError(err.message);
|
|
315
|
+
setLoading(false);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (history.length > 0) {
|
|
321
|
+
// Small delay to ensure canvas is fully mounted
|
|
322
|
+
const timer = setTimeout(() => {
|
|
323
|
+
renderChart();
|
|
324
|
+
}, 100);
|
|
325
|
+
return () => clearTimeout(timer);
|
|
326
|
+
}
|
|
327
|
+
}, [history]);
|
|
328
|
+
|
|
329
|
+
function renderChart() {
|
|
330
|
+
const ctx = document.getElementById('complianceChart');
|
|
331
|
+
if (!ctx) {
|
|
332
|
+
console.warn('Chart canvas not found');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
// Destroy existing chart
|
|
338
|
+
const existing = Chart.getChart(ctx);
|
|
339
|
+
if (existing) {
|
|
340
|
+
existing.destroy();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Sort history by date
|
|
344
|
+
const sorted = [...history].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
345
|
+
|
|
346
|
+
// Format dates for better readability
|
|
347
|
+
const formatDate = (timestamp) => {
|
|
348
|
+
const date = new Date(timestamp);
|
|
349
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
new Chart(ctx, {
|
|
353
|
+
type: 'line',
|
|
354
|
+
data: {
|
|
355
|
+
labels: sorted.map(h => formatDate(h.timestamp)),
|
|
356
|
+
datasets: [{
|
|
357
|
+
label: 'Compliance %',
|
|
358
|
+
data: sorted.map(h => h.report.summary.compliance),
|
|
359
|
+
borderColor: '#667eea',
|
|
360
|
+
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
|
361
|
+
tension: 0.4,
|
|
362
|
+
fill: true,
|
|
363
|
+
pointRadius: 4,
|
|
364
|
+
pointHoverRadius: 6,
|
|
365
|
+
}]
|
|
366
|
+
},
|
|
367
|
+
options: {
|
|
368
|
+
responsive: true,
|
|
369
|
+
maintainAspectRatio: false,
|
|
370
|
+
plugins: {
|
|
371
|
+
legend: {
|
|
372
|
+
display: false,
|
|
373
|
+
},
|
|
374
|
+
tooltip: {
|
|
375
|
+
callbacks: {
|
|
376
|
+
label: (context) => `Compliance: ${context.parsed.y}%`
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
scales: {
|
|
381
|
+
y: {
|
|
382
|
+
beginAtZero: true,
|
|
383
|
+
max: 100,
|
|
384
|
+
ticks: {
|
|
385
|
+
callback: (value) => value + '%'
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
x: {
|
|
389
|
+
ticks: {
|
|
390
|
+
maxRotation: 45,
|
|
391
|
+
minRotation: 45
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error('Failed to render chart:', error);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (loading) {
|
|
403
|
+
return <div className="loading">Loading dashboard data...</div>;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (error) {
|
|
407
|
+
return (
|
|
408
|
+
<div className="container">
|
|
409
|
+
<div className="error">
|
|
410
|
+
<strong>Error:</strong> {error}
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!report) {
|
|
417
|
+
return (
|
|
418
|
+
<div className="container">
|
|
419
|
+
<div className="error">No report data available. Run `specbridge report` to generate a report.</div>
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<>
|
|
426
|
+
<div className="header">
|
|
427
|
+
<h1>📊 SpecBridge Compliance Dashboard</h1>
|
|
428
|
+
<p>{report.project} - {new Date(report.timestamp).toLocaleString()}</p>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div className="container">
|
|
432
|
+
{/* Summary Cards */}
|
|
433
|
+
<div className="grid">
|
|
434
|
+
<div className="card">
|
|
435
|
+
<div className="compliance-score">
|
|
436
|
+
<div className="score">{report.summary.compliance}%</div>
|
|
437
|
+
<div className="label">Overall Compliance</div>
|
|
438
|
+
{analytics && (
|
|
439
|
+
<div style={{ marginTop: '1rem' }}>
|
|
440
|
+
<span className={`trend-indicator ${analytics.overallTrend}`}>
|
|
441
|
+
{analytics.overallTrend === 'up' ? '📈' : analytics.overallTrend === 'down' ? '📉' : '➡️'}
|
|
442
|
+
{' '}
|
|
443
|
+
{analytics.overallTrend.toUpperCase()}
|
|
444
|
+
</span>
|
|
445
|
+
</div>
|
|
446
|
+
)}
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<div className="card">
|
|
451
|
+
<h2>📋 Decisions</h2>
|
|
452
|
+
<div className="metric">
|
|
453
|
+
<span className="metric-label">Total Decisions</span>
|
|
454
|
+
<span className="metric-value">{report.summary.totalDecisions}</span>
|
|
455
|
+
</div>
|
|
456
|
+
<div className="metric">
|
|
457
|
+
<span className="metric-label">Active Decisions</span>
|
|
458
|
+
<span className="metric-value success">{report.summary.activeDecisions}</span>
|
|
459
|
+
</div>
|
|
460
|
+
<div className="metric">
|
|
461
|
+
<span className="metric-label">Total Constraints</span>
|
|
462
|
+
<span className="metric-value">{report.summary.totalConstraints}</span>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
<div className="card">
|
|
467
|
+
<h2>⚠️ Violations</h2>
|
|
468
|
+
<div className="metric">
|
|
469
|
+
<span className="metric-label">Critical</span>
|
|
470
|
+
<span className={`metric-value ${report.summary.violations.critical > 0 ? 'danger' : 'success'}`}>
|
|
471
|
+
{report.summary.violations.critical}
|
|
472
|
+
</span>
|
|
473
|
+
</div>
|
|
474
|
+
<div className="metric">
|
|
475
|
+
<span className="metric-label">High</span>
|
|
476
|
+
<span className={`metric-value ${report.summary.violations.high > 0 ? 'warning' : 'success'}`}>
|
|
477
|
+
{report.summary.violations.high}
|
|
478
|
+
</span>
|
|
479
|
+
</div>
|
|
480
|
+
<div className="metric">
|
|
481
|
+
<span className="metric-label">Medium</span>
|
|
482
|
+
<span className="metric-value">{report.summary.violations.medium}</span>
|
|
483
|
+
</div>
|
|
484
|
+
<div className="metric">
|
|
485
|
+
<span className="metric-label">Low</span>
|
|
486
|
+
<span className="metric-value">{report.summary.violations.low}</span>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
{/* Compliance Trend Chart */}
|
|
492
|
+
{history.length > 1 && (
|
|
493
|
+
<div className="card">
|
|
494
|
+
<h2>📈 Compliance Trend (30 Days)</h2>
|
|
495
|
+
<div className="chart-container">
|
|
496
|
+
<canvas id="complianceChart"></canvas>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
)}
|
|
500
|
+
|
|
501
|
+
{/* Decision Breakdown */}
|
|
502
|
+
<div className="card">
|
|
503
|
+
<h2>🎯 Decision Compliance</h2>
|
|
504
|
+
<ul className="decision-list">
|
|
505
|
+
{report.byDecision.map(decision => (
|
|
506
|
+
<li key={decision.decisionId} className="decision-item">
|
|
507
|
+
<div>
|
|
508
|
+
<div className="decision-name">{decision.title}</div>
|
|
509
|
+
<div style={{ marginTop: '0.25rem' }}>
|
|
510
|
+
{decision.violations > 0 && (
|
|
511
|
+
<span className="violation-badge medium">
|
|
512
|
+
{decision.violations} violation{decision.violations !== 1 ? 's' : ''}
|
|
513
|
+
</span>
|
|
514
|
+
)}
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
<div className={`decision-compliance ${decision.compliance === 100 ? 'success' : decision.compliance < 50 ? 'danger' : 'warning'}`}>
|
|
518
|
+
{decision.compliance}%
|
|
519
|
+
</div>
|
|
520
|
+
</li>
|
|
521
|
+
))}
|
|
522
|
+
</ul>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
{/* Insights */}
|
|
526
|
+
{analytics && analytics.insights && analytics.insights.length > 0 && (
|
|
527
|
+
<div className="card">
|
|
528
|
+
<h2>💡 Insights</h2>
|
|
529
|
+
<div className="insights">
|
|
530
|
+
{analytics.insights.map((insight, i) => (
|
|
531
|
+
<div key={i} className={`insight ${insight.type}`}>
|
|
532
|
+
<div className="insight-message">{insight.message}</div>
|
|
533
|
+
{insight.details && <div className="insight-details">{insight.details}</div>}
|
|
534
|
+
</div>
|
|
535
|
+
))}
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
)}
|
|
539
|
+
|
|
540
|
+
<div className="timestamp">
|
|
541
|
+
Last updated: {new Date(report.timestamp).toLocaleString()}
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
</>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
ReactDOM.render(<Dashboard />, document.getElementById('root'));
|
|
549
|
+
</script>
|
|
550
|
+
</body>
|
|
551
|
+
</html>
|
package/package.json
CHANGED