heicat 0.1.2
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/.eslintrc.js +29 -0
- package/README.md +346 -0
- package/examples/express-app/README.md +72 -0
- package/examples/express-app/contracts/auth.contract.json +38 -0
- package/examples/express-app/contracts/users.contract.json +49 -0
- package/examples/express-app/debug.js +13 -0
- package/examples/express-app/package-lock.json +913 -0
- package/examples/express-app/package.json +21 -0
- package/examples/express-app/server.js +116 -0
- package/jest.config.js +5 -0
- package/package.json +43 -0
- package/packages/cli/jest.config.js +7 -0
- package/packages/cli/package-lock.json +5041 -0
- package/packages/cli/package.json +37 -0
- package/packages/cli/src/cli.ts +49 -0
- package/packages/cli/src/commands/init.ts +103 -0
- package/packages/cli/src/commands/status.ts +75 -0
- package/packages/cli/src/commands/test.ts +188 -0
- package/packages/cli/src/commands/validate.ts +73 -0
- package/packages/cli/src/commands/watch.ts +655 -0
- package/packages/cli/src/index.ts +3 -0
- package/packages/cli/tsconfig.json +18 -0
- package/packages/core/jest.config.js +7 -0
- package/packages/core/package-lock.json +4581 -0
- package/packages/core/package.json +45 -0
- package/packages/core/src/__tests__/contract-loader.test.ts +112 -0
- package/packages/core/src/__tests__/validation-engine.test.ts +213 -0
- package/packages/core/src/contract-loader.ts +55 -0
- package/packages/core/src/engine.ts +95 -0
- package/packages/core/src/index.ts +9 -0
- package/packages/core/src/middleware.ts +97 -0
- package/packages/core/src/types/contract.ts +28 -0
- package/packages/core/src/types/options.ts +7 -0
- package/packages/core/src/types/violation.ts +19 -0
- package/packages/core/src/validation-engine.ts +157 -0
- package/packages/core/src/violation-store.ts +46 -0
- package/packages/core/tsconfig.json +18 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { createServer } from 'http';
|
|
4
|
+
import { readFileSync, watch as fsWatch, readdirSync } from 'fs';
|
|
5
|
+
import { resolve, extname } from 'path';
|
|
6
|
+
import { ContractEngine, loadContracts } from '@contract-studio/core';
|
|
7
|
+
|
|
8
|
+
export function watchCommand(options: { contractsPath: string; port: string }) {
|
|
9
|
+
const contractsPath = resolve(process.cwd(), options.contractsPath);
|
|
10
|
+
const port = parseInt(options.port);
|
|
11
|
+
|
|
12
|
+
console.log(chalk.blue('š Starting Heicat Watch Mode'));
|
|
13
|
+
console.log(chalk.gray(`š Watching: ${contractsPath}`));
|
|
14
|
+
console.log(chalk.gray(`š GUI Server: http://localhost:${port}`));
|
|
15
|
+
|
|
16
|
+
let engine: ContractEngine;
|
|
17
|
+
let contracts: any[] = [];
|
|
18
|
+
|
|
19
|
+
// Initialize engine
|
|
20
|
+
const initEngine = async () => {
|
|
21
|
+
try {
|
|
22
|
+
contracts = await loadContracts(contractsPath);
|
|
23
|
+
engine = new ContractEngine('dev');
|
|
24
|
+
await engine.loadContracts(contractsPath);
|
|
25
|
+
console.log(chalk.green(`ā
Loaded ${contracts.length} contracts`));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(chalk.red('ā Failed to initialize:', error));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Start GUI server
|
|
33
|
+
const server = createServer((req, res) => {
|
|
34
|
+
if (req.url === '/' || req.url === '/index.html') {
|
|
35
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
36
|
+
res.end(generateHTML());
|
|
37
|
+
} else if (req.url === '/api/violations') {
|
|
38
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
39
|
+
res.end(JSON.stringify({
|
|
40
|
+
violations: engine?.getViolations() || [],
|
|
41
|
+
stats: engine?.getStats() || { total: 0, errors: 0, warnings: 0 }
|
|
42
|
+
}));
|
|
43
|
+
} else if (req.url === '/api/contracts') {
|
|
44
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
45
|
+
res.end(JSON.stringify(contracts));
|
|
46
|
+
} else {
|
|
47
|
+
res.writeHead(404);
|
|
48
|
+
res.end('Not found');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
server.listen(port, async () => {
|
|
53
|
+
await initEngine();
|
|
54
|
+
|
|
55
|
+
// Watch for contract file changes
|
|
56
|
+
fsWatch(contractsPath, { recursive: true }, async (eventType, filename) => {
|
|
57
|
+
if (filename && extname(filename) === '.json' && filename.endsWith('.contract.json')) {
|
|
58
|
+
console.log(chalk.blue(`š Contract changed: ${filename}`));
|
|
59
|
+
try {
|
|
60
|
+
await initEngine();
|
|
61
|
+
console.log(chalk.green('ā
Reloaded contracts'));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(chalk.red('ā Failed to reload contracts:', error));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
console.log(chalk.green(`\nš Heicat watch mode active!`));
|
|
69
|
+
console.log(chalk.gray(`Open http://localhost:${port} to see violations`));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Handle graceful shutdown
|
|
73
|
+
process.on('SIGINT', () => {
|
|
74
|
+
console.log(chalk.blue('\nš Shutting down Heicat watch mode'));
|
|
75
|
+
server.close();
|
|
76
|
+
process.exit(0);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function generateHTML(): string {
|
|
81
|
+
return `
|
|
82
|
+
<!DOCTYPE html>
|
|
83
|
+
<html lang="en">
|
|
84
|
+
<head>
|
|
85
|
+
<meta charset="UTF-8">
|
|
86
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
87
|
+
<title>Heicat - Violations Dashboard</title>
|
|
88
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
89
|
+
<style>
|
|
90
|
+
:root {
|
|
91
|
+
--background: 222.2 84% 4.9%;
|
|
92
|
+
--foreground: 210 40% 98%;
|
|
93
|
+
--card: 222.2 84% 4.9%;
|
|
94
|
+
--card-foreground: 210 40% 98%;
|
|
95
|
+
--popover: 222.2 84% 4.9%;
|
|
96
|
+
--popover-foreground: 210 40% 98%;
|
|
97
|
+
--primary: 210 40% 96%;
|
|
98
|
+
--primary-foreground: 222.2 84% 4.9%;
|
|
99
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
100
|
+
--secondary-foreground: 210 40% 98%;
|
|
101
|
+
--muted: 217.2 32.6% 17.5%;
|
|
102
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
103
|
+
--accent: 217.2 32.6% 17.5%;
|
|
104
|
+
--accent-foreground: 210 40% 98%;
|
|
105
|
+
--destructive: 0 62.8% 30.6%;
|
|
106
|
+
--destructive-foreground: 210 40% 98%;
|
|
107
|
+
--border: 217.2 32.6% 17.5%;
|
|
108
|
+
--input: 217.2 32.6% 17.5%;
|
|
109
|
+
--ring: 212.7 26.8% 83.9%;
|
|
110
|
+
--radius: 0.5rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
* {
|
|
114
|
+
margin: 0;
|
|
115
|
+
padding: 0;
|
|
116
|
+
box-sizing: border-box;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
html {
|
|
120
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
body {
|
|
124
|
+
background-color: hsl(var(--background));
|
|
125
|
+
color: hsl(var(--foreground));
|
|
126
|
+
font-feature-settings: "rlig" 1, "calt" 1;
|
|
127
|
+
line-height: 1.5;
|
|
128
|
+
min-height: 100vh;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.container {
|
|
132
|
+
max-width: 1200px;
|
|
133
|
+
margin: 0 auto;
|
|
134
|
+
padding: 1.5rem;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Header */
|
|
138
|
+
.header {
|
|
139
|
+
background: hsl(var(--background));
|
|
140
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
141
|
+
padding: 2rem 0;
|
|
142
|
+
margin-bottom: 2rem;
|
|
143
|
+
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.header-content {
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
justify-content: space-between;
|
|
150
|
+
gap: 2rem;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.header-left {
|
|
154
|
+
display: flex;
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
gap: 0.5rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.header-title {
|
|
160
|
+
font-size: 2rem;
|
|
161
|
+
font-weight: 700;
|
|
162
|
+
color: hsl(var(--foreground));
|
|
163
|
+
letter-spacing: -0.025em;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.header-subtitle {
|
|
167
|
+
font-size: 1rem;
|
|
168
|
+
color: hsl(var(--muted-foreground));
|
|
169
|
+
font-weight: 400;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.header-actions {
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 1rem;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.status-indicator {
|
|
179
|
+
display: inline-flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
gap: 0.5rem;
|
|
182
|
+
padding: 0.5rem 1rem;
|
|
183
|
+
border-radius: calc(var(--radius) - 0.125rem);
|
|
184
|
+
font-size: 0.875rem;
|
|
185
|
+
font-weight: 500;
|
|
186
|
+
background: hsl(142.1 76.2% 36.3% / 0.1);
|
|
187
|
+
color: hsl(142.1 76.2% 36.3%);
|
|
188
|
+
border: 1px solid hsl(142.1 76.2% 36.3% / 0.2);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.status-dot {
|
|
192
|
+
width: 0.5rem;
|
|
193
|
+
height: 0.5rem;
|
|
194
|
+
border-radius: 50%;
|
|
195
|
+
background: hsl(142.1 76.2% 36.3%);
|
|
196
|
+
animation: pulse 2s infinite;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@keyframes pulse {
|
|
200
|
+
0%, 100% { opacity: 1; }
|
|
201
|
+
50% { opacity: 0.5; }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* Stats Grid */
|
|
205
|
+
.stats-grid {
|
|
206
|
+
display: grid;
|
|
207
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
208
|
+
gap: 1.5rem;
|
|
209
|
+
margin-bottom: 2rem;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.stat-card {
|
|
213
|
+
background: hsl(var(--card));
|
|
214
|
+
border: 1px solid hsl(var(--border));
|
|
215
|
+
border-radius: calc(var(--radius) + 0.125rem);
|
|
216
|
+
padding: 2rem;
|
|
217
|
+
transition: all 0.2s ease-in-out;
|
|
218
|
+
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
219
|
+
position: relative;
|
|
220
|
+
overflow: hidden;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.stat-card::before {
|
|
224
|
+
content: '';
|
|
225
|
+
position: absolute;
|
|
226
|
+
top: 0;
|
|
227
|
+
left: 0;
|
|
228
|
+
right: 0;
|
|
229
|
+
height: 4px;
|
|
230
|
+
background: linear-gradient(90deg, hsl(var(--primary)) 0%, hsl(var(--primary) / 0.8) 100%);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.stat-card:hover {
|
|
234
|
+
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
235
|
+
transform: translateY(-2px);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.stat-content {
|
|
239
|
+
display: flex;
|
|
240
|
+
flex-direction: column;
|
|
241
|
+
gap: 0.5rem;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.stat-number {
|
|
245
|
+
font-size: 3rem;
|
|
246
|
+
font-weight: 800;
|
|
247
|
+
line-height: 1;
|
|
248
|
+
color: hsl(var(--foreground));
|
|
249
|
+
font-variant-numeric: tabular-nums;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.stat-label {
|
|
253
|
+
font-size: 0.875rem;
|
|
254
|
+
font-weight: 600;
|
|
255
|
+
color: hsl(var(--muted-foreground));
|
|
256
|
+
text-transform: uppercase;
|
|
257
|
+
letter-spacing: 0.1em;
|
|
258
|
+
margin: 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.stat-description {
|
|
262
|
+
font-size: 0.75rem;
|
|
263
|
+
color: hsl(var(--muted-foreground));
|
|
264
|
+
font-weight: 400;
|
|
265
|
+
margin: 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* Violations Section */
|
|
269
|
+
.violations-section {
|
|
270
|
+
background: hsl(var(--card));
|
|
271
|
+
border: 1px solid hsl(var(--border));
|
|
272
|
+
border-radius: calc(var(--radius) + 0.125rem);
|
|
273
|
+
overflow: hidden;
|
|
274
|
+
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.violations-header {
|
|
278
|
+
padding: 2rem;
|
|
279
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
280
|
+
background: hsl(var(--muted) / 0.2);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.violations-title {
|
|
284
|
+
font-size: 1.5rem;
|
|
285
|
+
font-weight: 700;
|
|
286
|
+
color: hsl(var(--foreground));
|
|
287
|
+
letter-spacing: -0.025em;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.violations-list {
|
|
291
|
+
max-height: 600px;
|
|
292
|
+
overflow-y: auto;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.violation-item {
|
|
296
|
+
padding: 2rem;
|
|
297
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
298
|
+
transition: background-color 0.15s ease;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.violation-item:last-child {
|
|
302
|
+
border-bottom: none;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.violation-item:hover {
|
|
306
|
+
background: hsl(var(--muted) / 0.3);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.violation-header {
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: flex-start;
|
|
312
|
+
justify-content: space-between;
|
|
313
|
+
gap: 1.5rem;
|
|
314
|
+
margin-bottom: 1rem;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.violation-endpoint {
|
|
318
|
+
display: flex;
|
|
319
|
+
flex-direction: column;
|
|
320
|
+
gap: 0.25rem;
|
|
321
|
+
flex: 1;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.violation-method {
|
|
325
|
+
font-size: 0.75rem;
|
|
326
|
+
font-weight: 700;
|
|
327
|
+
padding: 0.25rem 0.75rem;
|
|
328
|
+
background: hsl(var(--primary));
|
|
329
|
+
color: hsl(var(--primary-foreground));
|
|
330
|
+
border-radius: calc(var(--radius) - 0.125rem);
|
|
331
|
+
text-transform: uppercase;
|
|
332
|
+
letter-spacing: 0.1em;
|
|
333
|
+
width: fit-content;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.violation-path {
|
|
337
|
+
font-size: 1rem;
|
|
338
|
+
font-weight: 600;
|
|
339
|
+
color: hsl(var(--foreground));
|
|
340
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
341
|
+
letter-spacing: -0.025em;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.violation-severity {
|
|
345
|
+
padding: 0.5rem 1rem;
|
|
346
|
+
border-radius: calc(var(--radius) - 0.125rem);
|
|
347
|
+
font-size: 0.75rem;
|
|
348
|
+
font-weight: 700;
|
|
349
|
+
text-transform: uppercase;
|
|
350
|
+
letter-spacing: 0.1em;
|
|
351
|
+
flex-shrink: 0;
|
|
352
|
+
border: 1px solid;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.severity-error {
|
|
356
|
+
background: hsl(0 84.2% 60.2% / 0.05);
|
|
357
|
+
color: hsl(var(--destructive));
|
|
358
|
+
border-color: hsl(0 84.2% 60.2% / 0.2);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.severity-warning {
|
|
362
|
+
background: hsl(38 92% 50% / 0.05);
|
|
363
|
+
color: hsl(38 92% 50%);
|
|
364
|
+
border-color: hsl(38 92% 50% / 0.2);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.violation-message {
|
|
368
|
+
font-size: 0.875rem;
|
|
369
|
+
color: hsl(var(--muted-foreground));
|
|
370
|
+
line-height: 1.6;
|
|
371
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
372
|
+
background: hsl(var(--muted) / 0.4);
|
|
373
|
+
padding: 1rem;
|
|
374
|
+
border-radius: calc(var(--radius) - 0.125rem);
|
|
375
|
+
border: 1px solid hsl(var(--border));
|
|
376
|
+
margin-top: 0.5rem;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.no-violations {
|
|
380
|
+
text-align: center;
|
|
381
|
+
padding: 4rem 2rem;
|
|
382
|
+
color: hsl(var(--muted-foreground));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.no-violations-icon {
|
|
386
|
+
font-size: 3rem;
|
|
387
|
+
margin-bottom: 1.5rem;
|
|
388
|
+
display: block;
|
|
389
|
+
opacity: 0.5;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.no-violations-title {
|
|
393
|
+
font-size: 1.25rem;
|
|
394
|
+
font-weight: 700;
|
|
395
|
+
color: hsl(var(--foreground));
|
|
396
|
+
margin-bottom: 0.75rem;
|
|
397
|
+
letter-spacing: -0.025em;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.no-violations-text {
|
|
401
|
+
font-size: 0.875rem;
|
|
402
|
+
line-height: 1.5;
|
|
403
|
+
max-width: 400px;
|
|
404
|
+
margin: 0 auto;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* Buttons */
|
|
408
|
+
.btn {
|
|
409
|
+
display: inline-flex;
|
|
410
|
+
align-items: center;
|
|
411
|
+
justify-content: center;
|
|
412
|
+
gap: 0.5rem;
|
|
413
|
+
padding: 0.5rem 1rem;
|
|
414
|
+
border-radius: calc(var(--radius) - 0.125rem);
|
|
415
|
+
font-size: 0.875rem;
|
|
416
|
+
font-weight: 500;
|
|
417
|
+
transition: all 0.2s ease-in-out;
|
|
418
|
+
cursor: pointer;
|
|
419
|
+
border: 1px solid hsl(var(--border));
|
|
420
|
+
background: hsl(var(--background));
|
|
421
|
+
color: hsl(var(--foreground));
|
|
422
|
+
text-decoration: none;
|
|
423
|
+
white-space: nowrap;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.btn:hover {
|
|
427
|
+
background: hsl(var(--muted));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.btn:focus-visible {
|
|
431
|
+
outline: 2px solid hsl(var(--ring));
|
|
432
|
+
outline-offset: 2px;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.btn-primary {
|
|
436
|
+
background: hsl(var(--primary));
|
|
437
|
+
color: hsl(var(--primary-foreground));
|
|
438
|
+
border-color: hsl(var(--primary));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.btn-primary:hover {
|
|
442
|
+
background: hsl(var(--primary) / 0.9);
|
|
443
|
+
border-color: hsl(var(--primary) / 0.9);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.btn-ghost {
|
|
447
|
+
border-color: transparent;
|
|
448
|
+
background: transparent;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.btn-ghost:hover {
|
|
452
|
+
background: hsl(var(--muted));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* Responsive */
|
|
456
|
+
@media (max-width: 768px) {
|
|
457
|
+
.container {
|
|
458
|
+
padding: 1rem;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.header-content {
|
|
462
|
+
flex-direction: column;
|
|
463
|
+
align-items: flex-start;
|
|
464
|
+
gap: 1rem;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.stats-grid {
|
|
468
|
+
grid-template-columns: 1fr;
|
|
469
|
+
gap: 1rem;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.violation-header {
|
|
473
|
+
flex-direction: column;
|
|
474
|
+
align-items: flex-start;
|
|
475
|
+
gap: 0.75rem;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.violation-endpoint {
|
|
479
|
+
flex-direction: column;
|
|
480
|
+
align-items: flex-start;
|
|
481
|
+
gap: 0.5rem;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* Scrollbar */
|
|
486
|
+
.violations-list::-webkit-scrollbar {
|
|
487
|
+
width: 6px;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.violations-list::-webkit-scrollbar-track {
|
|
491
|
+
background: hsl(var(--muted));
|
|
492
|
+
border-radius: 3px;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.violations-list::-webkit-scrollbar-thumb {
|
|
496
|
+
background: hsl(var(--muted-foreground) / 0.3);
|
|
497
|
+
border-radius: 3px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.violations-list::-webkit-scrollbar-thumb:hover {
|
|
501
|
+
background: hsl(var(--muted-foreground) / 0.5);
|
|
502
|
+
}
|
|
503
|
+
</style>
|
|
504
|
+
</head>
|
|
505
|
+
<body>
|
|
506
|
+
<header class="header">
|
|
507
|
+
<div class="container">
|
|
508
|
+
<div class="header-content">
|
|
509
|
+
<div class="header-left">
|
|
510
|
+
<h1 class="header-title">Heicat</h1>
|
|
511
|
+
<p class="header-subtitle">Runtime API Contract Enforcement Dashboard</p>
|
|
512
|
+
<div class="status-indicator">
|
|
513
|
+
<div class="status-dot"></div>
|
|
514
|
+
<span>Monitoring Active</span>
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
<div class="header-actions">
|
|
518
|
+
<button class="btn btn-ghost" onclick="clearViolations()">
|
|
519
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
520
|
+
<polyline points="3,6 5,6 21,6"></polyline>
|
|
521
|
+
<path d="M19,6v14a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2v2"></path>
|
|
522
|
+
<line x1="10" y1="11" x2="10" y2="17"></line>
|
|
523
|
+
<line x1="14" y1="11" x2="14" y2="17"></line>
|
|
524
|
+
</svg>
|
|
525
|
+
Clear Logs
|
|
526
|
+
</button>
|
|
527
|
+
<button class="btn btn-primary" onclick="loadData()">
|
|
528
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
529
|
+
<polyline points="23,4 23,10 17,10"></polyline>
|
|
530
|
+
<polyline points="1,20 1,14 7,14"></polyline>
|
|
531
|
+
<path d="M20.49,9A9,9,0,0,0,5.64,5.64L1,10m22,4l-4.64,4.36A9,9,0,0,1,3.51,15"></path>
|
|
532
|
+
</svg>
|
|
533
|
+
Refresh Data
|
|
534
|
+
</button>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
</header>
|
|
539
|
+
|
|
540
|
+
<main class="container">
|
|
541
|
+
<div class="stats-grid" id="stats"></div>
|
|
542
|
+
|
|
543
|
+
<section class="violations-section">
|
|
544
|
+
<div class="violations-header">
|
|
545
|
+
<h2 class="violations-title">Contract Validation Log</h2>
|
|
546
|
+
</div>
|
|
547
|
+
<div class="violations-list" id="violations-list"></div>
|
|
548
|
+
</section>
|
|
549
|
+
</main>
|
|
550
|
+
|
|
551
|
+
<script>
|
|
552
|
+
let violations = [];
|
|
553
|
+
let stats = { total: 0, errors: 0, warnings: 0 };
|
|
554
|
+
|
|
555
|
+
async function loadData() {
|
|
556
|
+
try {
|
|
557
|
+
const response = await fetch('http://localhost:3000/api/violations');
|
|
558
|
+
const data = await response.json();
|
|
559
|
+
violations = data.violations || [];
|
|
560
|
+
// Parse API stats format to GUI format
|
|
561
|
+
const apiStats = data.stats || {};
|
|
562
|
+
stats = {
|
|
563
|
+
total: apiStats.total || 0,
|
|
564
|
+
errors: (apiStats.bySeverity && apiStats.bySeverity.error) || 0,
|
|
565
|
+
warnings: (apiStats.bySeverity && apiStats.bySeverity.warning) || 0
|
|
566
|
+
};
|
|
567
|
+
renderStats();
|
|
568
|
+
renderViolations();
|
|
569
|
+
} catch (error) {
|
|
570
|
+
console.error('Failed to load data:', error);
|
|
571
|
+
// Show error state in GUI
|
|
572
|
+
violations = [];
|
|
573
|
+
stats = { total: 0, errors: 0, warnings: 0 };
|
|
574
|
+
renderStats();
|
|
575
|
+
renderViolations();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function renderStats() {
|
|
580
|
+
document.getElementById('stats').innerHTML = \`
|
|
581
|
+
<div class="stat-card">
|
|
582
|
+
<div class="stat-content">
|
|
583
|
+
<div class="stat-number">\${stats.total}</div>
|
|
584
|
+
<h3 class="stat-label">Total Violations</h3>
|
|
585
|
+
<p class="stat-description">All contract validation issues</p>
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
<div class="stat-card">
|
|
589
|
+
<div class="stat-content">
|
|
590
|
+
<div class="stat-number" style="color: hsl(var(--destructive));">\${stats.errors}</div>
|
|
591
|
+
<h3 class="stat-label">Critical Errors</h3>
|
|
592
|
+
<p class="stat-description">Requests blocked by validation</p>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
<div class="stat-card">
|
|
596
|
+
<div class="stat-content">
|
|
597
|
+
<div class="stat-number" style="color: hsl(38 92% 50%);">\${stats.warnings}</div>
|
|
598
|
+
<h3 class="stat-label">Response Warnings</h3>
|
|
599
|
+
<p class="stat-description">Schema compliance issues</p>
|
|
600
|
+
</div>
|
|
601
|
+
</div>
|
|
602
|
+
\`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function clearViolations() {
|
|
606
|
+
try {
|
|
607
|
+
// In a real implementation, this would call an API endpoint to clear violations
|
|
608
|
+
// For now, we'll just refresh the data
|
|
609
|
+
violations = [];
|
|
610
|
+
stats = { total: 0, errors: 0, warnings: 0 };
|
|
611
|
+
renderStats();
|
|
612
|
+
renderViolations();
|
|
613
|
+
console.log('Violations cleared');
|
|
614
|
+
} catch (error) {
|
|
615
|
+
console.error('Failed to clear violations:', error);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function renderViolations() {
|
|
620
|
+
const list = document.getElementById('violations-list');
|
|
621
|
+
|
|
622
|
+
if (violations.length === 0) {
|
|
623
|
+
list.innerHTML = \`
|
|
624
|
+
<div class="no-violations">
|
|
625
|
+
<span class="no-violations-icon">ā</span>
|
|
626
|
+
<div class="no-violations-title">System Status: Normal</div>
|
|
627
|
+
<div class="no-violations-text">All API contracts are being enforced correctly. No validation issues detected.</div>
|
|
628
|
+
</div>
|
|
629
|
+
\`;
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
list.innerHTML = violations.slice(0, 50).map(v => \`
|
|
634
|
+
<div class="violation-item">
|
|
635
|
+
<div class="violation-header">
|
|
636
|
+
<div class="violation-endpoint">
|
|
637
|
+
<span class="violation-method">\${v.endpoint.method}</span>
|
|
638
|
+
<span class="violation-path">\${v.endpoint.path}</span>
|
|
639
|
+
</div>
|
|
640
|
+
<span class="violation-severity severity-\${v.severity}">\${v.severity}</span>
|
|
641
|
+
</div>
|
|
642
|
+
<div class="violation-message">\${v.message}</div>
|
|
643
|
+
</div>
|
|
644
|
+
\`).join('');
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Auto-refresh every 3 seconds
|
|
648
|
+
setInterval(loadData, 3000);
|
|
649
|
+
|
|
650
|
+
// Initial load
|
|
651
|
+
loadData();
|
|
652
|
+
</script>
|
|
653
|
+
</body>
|
|
654
|
+
</html>`;
|
|
655
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
18
|
+
}
|