@sentinel-atl/dashboard 0.1.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/README.md +51 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +345 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @sentinel-atl/dashboard
|
|
2
|
+
|
|
3
|
+
Web dashboard for visualizing trust graphs, reputation scores, delegation chains, and audit logs.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Trust graph** — interactive SVG visualization of agent nodes and trust edges
|
|
8
|
+
- **Reputation cards** — color-coded score bars per agent
|
|
9
|
+
- **Audit trail** — real-time table of last 20 events
|
|
10
|
+
- **Revocation stats** — revoked VCs, DIDs, and kill events
|
|
11
|
+
- **Offline indicator** — shows connectivity status
|
|
12
|
+
- **Zero dependencies** — embedded HTML/CSS/JS, dark theme, auto-refresh every 5s
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @sentinel-atl/dashboard
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createDashboard, buildDashboardData } from '@sentinel-atl/dashboard';
|
|
24
|
+
|
|
25
|
+
const server = await createDashboard({
|
|
26
|
+
port: 3000,
|
|
27
|
+
title: 'My Trust Dashboard',
|
|
28
|
+
getData: async () => buildDashboardData({
|
|
29
|
+
nodes: [{ id: 'did:key:z6Mk...', label: 'Agent A', type: 'agent' }],
|
|
30
|
+
edges: [{ from: 'did:key:z6MkA...', to: 'did:key:z6MkB...', label: 'trusts', weight: 0.9 }],
|
|
31
|
+
auditLog,
|
|
32
|
+
revocationManager,
|
|
33
|
+
offlineManager,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const { url } = await server.start();
|
|
38
|
+
console.log(`Dashboard: ${url}`);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
| Export | Description |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `createDashboard(config)` | Factory — creates a `DashboardServer` |
|
|
46
|
+
| `DashboardServer` | HTTP server serving the dashboard UI |
|
|
47
|
+
| `buildDashboardData(options)` | Build dashboard data from Sentinel components |
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sentinel-atl/dashboard — Trust Visualization Dashboard
|
|
3
|
+
*
|
|
4
|
+
* A lightweight, zero-dependency web dashboard for visualizing:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Trust Graph** — Agent DIDs as nodes, handshakes/delegations as edges
|
|
7
|
+
* 2. **Reputation Scores** — Real-time score cards with vouch history
|
|
8
|
+
* 3. **Delegation Chains** — Tree view of scope narrowing through delegation
|
|
9
|
+
* 4. **Audit Trail** — Searchable, filterable event log with hash-chain status
|
|
10
|
+
* 5. **Revocation Status** — Live view of revoked VCs/DIDs and kill events
|
|
11
|
+
* 6. **Offline Status** — Cache stats, pending transactions, CRDT state
|
|
12
|
+
*
|
|
13
|
+
* The dashboard serves a single HTML page with embedded CSS/JS.
|
|
14
|
+
* No build step, no bundler, no React — just Node.js HTTP.
|
|
15
|
+
*
|
|
16
|
+
* Blueprint ref: Phase 3, Milestone 3c (Dashboard)
|
|
17
|
+
*/
|
|
18
|
+
import type { ReputationScore } from '@sentinel-atl/reputation';
|
|
19
|
+
import type { AuditLog } from '@sentinel-atl/audit';
|
|
20
|
+
import type { RevocationManager } from '@sentinel-atl/revocation';
|
|
21
|
+
import type { OfflineManager } from '@sentinel-atl/offline';
|
|
22
|
+
export interface TrustGraphNode {
|
|
23
|
+
did: string;
|
|
24
|
+
label: string;
|
|
25
|
+
type: 'principal' | 'agent' | 'sub-agent';
|
|
26
|
+
reputation?: ReputationScore;
|
|
27
|
+
revoked?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface TrustGraphEdge {
|
|
30
|
+
from: string;
|
|
31
|
+
to: string;
|
|
32
|
+
type: 'authorization' | 'delegation' | 'handshake' | 'vouch';
|
|
33
|
+
label?: string;
|
|
34
|
+
scope?: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface DashboardData {
|
|
37
|
+
nodes: TrustGraphNode[];
|
|
38
|
+
edges: TrustGraphEdge[];
|
|
39
|
+
auditEntries: AuditEntry[];
|
|
40
|
+
revocationStats: {
|
|
41
|
+
revokedVCs: number;
|
|
42
|
+
revokedDIDs: number;
|
|
43
|
+
killEvents: number;
|
|
44
|
+
};
|
|
45
|
+
offlineStats?: {
|
|
46
|
+
vcCacheSize: number;
|
|
47
|
+
reputationCacheSize: number;
|
|
48
|
+
pendingTransactions: number;
|
|
49
|
+
crdtEntries: number;
|
|
50
|
+
isOnline: boolean;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export interface AuditEntry {
|
|
54
|
+
timestamp: string;
|
|
55
|
+
eventType: string;
|
|
56
|
+
actorDid: string;
|
|
57
|
+
targetDid?: string;
|
|
58
|
+
result: string;
|
|
59
|
+
reason?: string;
|
|
60
|
+
metadata?: Record<string, unknown>;
|
|
61
|
+
prevHash?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface DashboardConfig {
|
|
64
|
+
/** Port to serve on (default: 3000) */
|
|
65
|
+
port?: number;
|
|
66
|
+
/** Host to bind to (default: localhost) */
|
|
67
|
+
host?: string;
|
|
68
|
+
/** Dashboard title */
|
|
69
|
+
title?: string;
|
|
70
|
+
/** Data source — call this to get fresh data */
|
|
71
|
+
getData: () => Promise<DashboardData> | DashboardData;
|
|
72
|
+
}
|
|
73
|
+
export declare class DashboardServer {
|
|
74
|
+
private config;
|
|
75
|
+
private server;
|
|
76
|
+
constructor(config: DashboardConfig);
|
|
77
|
+
/**
|
|
78
|
+
* Start the dashboard server.
|
|
79
|
+
*/
|
|
80
|
+
start(): Promise<{
|
|
81
|
+
url: string;
|
|
82
|
+
}>;
|
|
83
|
+
/**
|
|
84
|
+
* Stop the dashboard server.
|
|
85
|
+
*/
|
|
86
|
+
stop(): Promise<void>;
|
|
87
|
+
private handleRequest;
|
|
88
|
+
private renderHTML;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Create and start a dashboard server.
|
|
92
|
+
*/
|
|
93
|
+
export declare function createDashboard(config: DashboardConfig): Promise<DashboardServer>;
|
|
94
|
+
/**
|
|
95
|
+
* Helper: build DashboardData from Sentinel components.
|
|
96
|
+
*/
|
|
97
|
+
export declare function buildDashboardData(options: {
|
|
98
|
+
nodes: TrustGraphNode[];
|
|
99
|
+
edges: TrustGraphEdge[];
|
|
100
|
+
auditLog?: AuditLog;
|
|
101
|
+
revocationManager?: RevocationManager;
|
|
102
|
+
offlineManager?: OfflineManager;
|
|
103
|
+
}): Promise<DashboardData>;
|
|
104
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sentinel-atl/dashboard — Trust Visualization Dashboard
|
|
3
|
+
*
|
|
4
|
+
* A lightweight, zero-dependency web dashboard for visualizing:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Trust Graph** — Agent DIDs as nodes, handshakes/delegations as edges
|
|
7
|
+
* 2. **Reputation Scores** — Real-time score cards with vouch history
|
|
8
|
+
* 3. **Delegation Chains** — Tree view of scope narrowing through delegation
|
|
9
|
+
* 4. **Audit Trail** — Searchable, filterable event log with hash-chain status
|
|
10
|
+
* 5. **Revocation Status** — Live view of revoked VCs/DIDs and kill events
|
|
11
|
+
* 6. **Offline Status** — Cache stats, pending transactions, CRDT state
|
|
12
|
+
*
|
|
13
|
+
* The dashboard serves a single HTML page with embedded CSS/JS.
|
|
14
|
+
* No build step, no bundler, no React — just Node.js HTTP.
|
|
15
|
+
*
|
|
16
|
+
* Blueprint ref: Phase 3, Milestone 3c (Dashboard)
|
|
17
|
+
*/
|
|
18
|
+
import { createServer } from 'node:http';
|
|
19
|
+
// ─── Dashboard Server ────────────────────────────────────────────────
|
|
20
|
+
export class DashboardServer {
|
|
21
|
+
config;
|
|
22
|
+
server = null;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.config = {
|
|
25
|
+
port: 3000,
|
|
26
|
+
host: 'localhost',
|
|
27
|
+
title: 'Sentinel Trust Dashboard',
|
|
28
|
+
...config,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Start the dashboard server.
|
|
33
|
+
*/
|
|
34
|
+
async start() {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
this.server = createServer(async (req, res) => {
|
|
37
|
+
await this.handleRequest(req, res);
|
|
38
|
+
});
|
|
39
|
+
this.server.listen(this.config.port, this.config.host, () => {
|
|
40
|
+
const url = `http://${this.config.host}:${this.config.port}`;
|
|
41
|
+
resolve({ url });
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Stop the dashboard server.
|
|
47
|
+
*/
|
|
48
|
+
async stop() {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
if (!this.server)
|
|
51
|
+
return resolve();
|
|
52
|
+
this.server.close((err) => {
|
|
53
|
+
if (err)
|
|
54
|
+
reject(err);
|
|
55
|
+
else
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async handleRequest(req, res) {
|
|
61
|
+
const url = req.url ?? '/';
|
|
62
|
+
if (url === '/api/data') {
|
|
63
|
+
try {
|
|
64
|
+
const data = await this.config.getData();
|
|
65
|
+
res.writeHead(200, {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
'Cache-Control': 'no-cache',
|
|
68
|
+
});
|
|
69
|
+
res.end(JSON.stringify(data));
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
73
|
+
res.end(JSON.stringify({ error: 'Failed to load data' }));
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Serve the dashboard HTML
|
|
78
|
+
res.writeHead(200, {
|
|
79
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
80
|
+
'Cache-Control': 'no-cache',
|
|
81
|
+
});
|
|
82
|
+
res.end(this.renderHTML());
|
|
83
|
+
}
|
|
84
|
+
renderHTML() {
|
|
85
|
+
return `<!DOCTYPE html>
|
|
86
|
+
<html lang="en">
|
|
87
|
+
<head>
|
|
88
|
+
<meta charset="utf-8">
|
|
89
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
90
|
+
<title>${escapeHtml(this.config.title)}</title>
|
|
91
|
+
<style>
|
|
92
|
+
:root {
|
|
93
|
+
--bg: #0d1117; --surface: #161b22; --border: #30363d;
|
|
94
|
+
--text: #e6edf3; --muted: #8b949e; --accent: #58a6ff;
|
|
95
|
+
--green: #3fb950; --red: #f85149; --yellow: #d29922; --purple: #bc8cff;
|
|
96
|
+
}
|
|
97
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
98
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
99
|
+
background: var(--bg); color: var(--text); line-height: 1.5; }
|
|
100
|
+
.header { background: var(--surface); border-bottom: 1px solid var(--border);
|
|
101
|
+
padding: 16px 24px; display: flex; align-items: center; gap: 12px; }
|
|
102
|
+
.header h1 { font-size: 20px; font-weight: 600; }
|
|
103
|
+
.header .shield { font-size: 24px; }
|
|
104
|
+
.header .status { margin-left: auto; font-size: 13px; padding: 4px 12px;
|
|
105
|
+
border-radius: 12px; font-weight: 500; }
|
|
106
|
+
.header .status.online { background: #0d1f0d; color: var(--green); }
|
|
107
|
+
.header .status.offline { background: #2d1515; color: var(--red); }
|
|
108
|
+
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
|
|
109
|
+
padding: 24px; max-width: 1400px; margin: 0 auto; }
|
|
110
|
+
.card { background: var(--surface); border: 1px solid var(--border);
|
|
111
|
+
border-radius: 8px; padding: 16px; }
|
|
112
|
+
.card h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px;
|
|
113
|
+
color: var(--muted); margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
|
|
114
|
+
.card.full { grid-column: 1 / -1; }
|
|
115
|
+
.node { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px;
|
|
116
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
|
|
117
|
+
margin: 4px; font-size: 13px; }
|
|
118
|
+
.node .icon { font-size: 16px; }
|
|
119
|
+
.node .did { font-family: monospace; font-size: 11px; color: var(--muted); }
|
|
120
|
+
.node.revoked { border-color: var(--red); opacity: 0.6; text-decoration: line-through; }
|
|
121
|
+
.edge { font-size: 12px; color: var(--muted); padding: 4px 0; border-bottom: 1px solid var(--border); }
|
|
122
|
+
.edge .type { font-weight: 600; color: var(--accent); }
|
|
123
|
+
.score-card { display: flex; align-items: center; gap: 12px; padding: 8px 12px;
|
|
124
|
+
background: var(--bg); border-radius: 6px; margin-bottom: 8px; }
|
|
125
|
+
.score-bar { flex: 1; height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; }
|
|
126
|
+
.score-bar .fill { height: 100%; border-radius: 4px; transition: width 0.3s; }
|
|
127
|
+
.score-val { font-weight: 700; font-size: 18px; min-width: 40px; text-align: right; }
|
|
128
|
+
.audit-row { font-size: 12px; padding: 6px 0; border-bottom: 1px solid var(--border);
|
|
129
|
+
display: grid; grid-template-columns: 140px 160px 1fr 80px; gap: 8px; align-items: center; }
|
|
130
|
+
.audit-row .ts { color: var(--muted); font-family: monospace; font-size: 11px; }
|
|
131
|
+
.audit-row .event { font-weight: 600; }
|
|
132
|
+
.audit-row .result.success { color: var(--green); }
|
|
133
|
+
.audit-row .result.failure { color: var(--red); }
|
|
134
|
+
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; }
|
|
135
|
+
.stat { background: var(--bg); border-radius: 6px; padding: 12px; text-align: center; }
|
|
136
|
+
.stat .value { font-size: 28px; font-weight: 700; }
|
|
137
|
+
.stat .label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
138
|
+
.stat.danger .value { color: var(--red); }
|
|
139
|
+
.stat.warn .value { color: var(--yellow); }
|
|
140
|
+
.stat.good .value { color: var(--green); }
|
|
141
|
+
.graph-canvas { width: 100%; height: 300px; position: relative; }
|
|
142
|
+
.graph-node { position: absolute; padding: 8px 12px; background: var(--bg);
|
|
143
|
+
border: 2px solid var(--accent); border-radius: 8px; font-size: 12px;
|
|
144
|
+
cursor: default; text-align: center; z-index: 2; }
|
|
145
|
+
.graph-node.principal { border-color: var(--purple); }
|
|
146
|
+
.graph-node.revoked { border-color: var(--red); opacity: 0.5; }
|
|
147
|
+
svg.graph-edges { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
|
|
148
|
+
svg.graph-edges line { stroke: var(--border); stroke-width: 2; }
|
|
149
|
+
svg.graph-edges line.auth { stroke: var(--purple); }
|
|
150
|
+
svg.graph-edges line.delegation { stroke: var(--accent); }
|
|
151
|
+
svg.graph-edges line.handshake { stroke: var(--green); }
|
|
152
|
+
svg.graph-edges line.vouch { stroke: var(--yellow); stroke-dasharray: 4; }
|
|
153
|
+
.empty { color: var(--muted); font-style: italic; font-size: 13px; padding: 12px; }
|
|
154
|
+
.refresh-btn { background: var(--accent); color: #fff; border: none; padding: 6px 16px;
|
|
155
|
+
border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 600; }
|
|
156
|
+
.refresh-btn:hover { opacity: 0.9; }
|
|
157
|
+
@media (max-width: 768px) { .grid { grid-template-columns: 1fr; } }
|
|
158
|
+
</style>
|
|
159
|
+
</head>
|
|
160
|
+
<body>
|
|
161
|
+
<div class="header">
|
|
162
|
+
<span class="shield">🛡️</span>
|
|
163
|
+
<h1>${escapeHtml(this.config.title)}</h1>
|
|
164
|
+
<span id="status" class="status online">● Online</span>
|
|
165
|
+
<button class="refresh-btn" onclick="loadData()">↻ Refresh</button>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="grid" id="grid">
|
|
168
|
+
<div class="card"><h2>Loading...</h2></div>
|
|
169
|
+
</div>
|
|
170
|
+
<script>
|
|
171
|
+
let data = null;
|
|
172
|
+
|
|
173
|
+
async function loadData() {
|
|
174
|
+
try {
|
|
175
|
+
const res = await fetch('/api/data');
|
|
176
|
+
data = await res.json();
|
|
177
|
+
render();
|
|
178
|
+
} catch(e) {
|
|
179
|
+
document.getElementById('grid').innerHTML = '<div class="card full"><h2>⚠ Error loading data</h2></div>';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function did(d) { return d ? d.slice(0, 20) + '...' : '—'; }
|
|
184
|
+
function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
185
|
+
|
|
186
|
+
function scoreColor(s) {
|
|
187
|
+
if (s >= 70) return 'var(--green)';
|
|
188
|
+
if (s >= 40) return 'var(--yellow)';
|
|
189
|
+
return 'var(--red)';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function render() {
|
|
193
|
+
if (!data) return;
|
|
194
|
+
const g = document.getElementById('grid');
|
|
195
|
+
const online = data.offlineStats ? data.offlineStats.isOnline : true;
|
|
196
|
+
document.getElementById('status').className = 'status ' + (online ? 'online' : 'offline');
|
|
197
|
+
document.getElementById('status').textContent = online ? '● Online' : '● Offline';
|
|
198
|
+
|
|
199
|
+
g.innerHTML = renderStats() + renderGraph() + renderReputation() + renderAudit() + renderRevocation();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function renderStats() {
|
|
203
|
+
const n = data.nodes.length;
|
|
204
|
+
const e = data.edges.length;
|
|
205
|
+
const a = data.auditEntries.length;
|
|
206
|
+
const rv = data.revocationStats;
|
|
207
|
+
const off = data.offlineStats;
|
|
208
|
+
return '<div class="card full"><h2>📊 Overview</h2><div class="stat-grid">'
|
|
209
|
+
+ stat(n, 'Agents', 'good') + stat(e, 'Connections', 'good')
|
|
210
|
+
+ stat(a, 'Audit Events', '') + stat(rv.revokedVCs, 'Revoked VCs', rv.revokedVCs > 0 ? 'danger' : '')
|
|
211
|
+
+ stat(rv.revokedDIDs, 'Revoked DIDs', rv.revokedDIDs > 0 ? 'danger' : '')
|
|
212
|
+
+ stat(rv.killEvents, 'Kill Events', rv.killEvents > 0 ? 'danger' : '')
|
|
213
|
+
+ (off ? stat(off.pendingTransactions, 'Pending Tx', off.pendingTransactions > 0 ? 'warn' : '')
|
|
214
|
+
+ stat(off.crdtEntries, 'CRDT Entries', '') : '')
|
|
215
|
+
+ '</div></div>';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function stat(v, l, cls) {
|
|
219
|
+
return '<div class="stat ' + cls + '"><div class="value">' + v + '</div><div class="label">' + l + '</div></div>';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function renderGraph() {
|
|
223
|
+
if (data.nodes.length === 0) return '<div class="card"><h2>🔗 Trust Graph</h2><p class="empty">No agents</p></div>';
|
|
224
|
+
const W = 600, H = 300;
|
|
225
|
+
const cx = W/2, cy = H/2;
|
|
226
|
+
const positions = {};
|
|
227
|
+
data.nodes.forEach(function(n, i) {
|
|
228
|
+
const angle = (2 * Math.PI * i) / data.nodes.length - Math.PI/2;
|
|
229
|
+
const r = Math.min(W, H) * 0.35;
|
|
230
|
+
positions[n.did] = { x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle) };
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
let svgLines = '';
|
|
234
|
+
data.edges.forEach(function(e) {
|
|
235
|
+
const p1 = positions[e.from]; const p2 = positions[e.to];
|
|
236
|
+
if (p1 && p2) svgLines += '<line x1="'+p1.x+'" y1="'+p1.y+'" x2="'+p2.x+'" y2="'+p2.y+'" class="'+e.type+'"/>';
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
let nodes = '';
|
|
240
|
+
data.nodes.forEach(function(n) {
|
|
241
|
+
const p = positions[n.did];
|
|
242
|
+
const cls = (n.type === 'principal' ? ' principal' : '') + (n.revoked ? ' revoked' : '');
|
|
243
|
+
const icon = n.type === 'principal' ? '👤' : '🤖';
|
|
244
|
+
nodes += '<div class="graph-node' + cls + '" style="left:' + (p.x-40) + 'px;top:' + (p.y-20) + 'px">'
|
|
245
|
+
+ icon + ' ' + escHtml(n.label) + '<br><span style="font-size:10px;color:var(--muted)">' + did(n.did) + '</span></div>';
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return '<div class="card"><h2>🔗 Trust Graph</h2>'
|
|
249
|
+
+ '<div class="graph-canvas"><svg class="graph-edges" viewBox="0 0 '+W+' '+H+'">' + svgLines + '</svg>' + nodes + '</div></div>';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderReputation() {
|
|
253
|
+
const agents = data.nodes.filter(function(n) { return n.reputation; });
|
|
254
|
+
if (agents.length === 0) return '<div class="card"><h2>📈 Reputation</h2><p class="empty">No reputation data</p></div>';
|
|
255
|
+
let html = '<div class="card"><h2>📈 Reputation Scores</h2>';
|
|
256
|
+
agents.forEach(function(n) {
|
|
257
|
+
const r = n.reputation;
|
|
258
|
+
const q = r.isQuarantined ? ' <span style="color:var(--red)">[QUARANTINED]</span>' : '';
|
|
259
|
+
html += '<div class="score-card"><div style="min-width:120px">' + escHtml(n.label) + q + '</div>'
|
|
260
|
+
+ '<div class="score-bar"><div class="fill" style="width:'+r.score+'%;background:'+scoreColor(r.score)+'"></div></div>'
|
|
261
|
+
+ '<div class="score-val" style="color:'+scoreColor(r.score)+'">' + r.score + '</div></div>';
|
|
262
|
+
});
|
|
263
|
+
return html + '</div>';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function renderAudit() {
|
|
267
|
+
const entries = data.auditEntries.slice(-20).reverse();
|
|
268
|
+
if (entries.length === 0) return '<div class="card full"><h2>📋 Audit Log</h2><p class="empty">No events</p></div>';
|
|
269
|
+
let html = '<div class="card full"><h2>📋 Audit Log (last 20)</h2>';
|
|
270
|
+
html += '<div class="audit-row" style="font-weight:600;color:var(--muted)"><span>Timestamp</span><span>Event</span><span>Actor</span><span>Result</span></div>';
|
|
271
|
+
entries.forEach(function(e) {
|
|
272
|
+
const ts = e.timestamp ? new Date(e.timestamp).toLocaleTimeString() : '—';
|
|
273
|
+
html += '<div class="audit-row"><span class="ts">' + ts + '</span>'
|
|
274
|
+
+ '<span class="event">' + escHtml(e.eventType) + '</span>'
|
|
275
|
+
+ '<span style="font-family:monospace;font-size:11px">' + did(e.actorDid) + '</span>'
|
|
276
|
+
+ '<span class="result ' + e.result + '">' + e.result + '</span></div>';
|
|
277
|
+
});
|
|
278
|
+
return html + '</div>';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function renderRevocation() {
|
|
282
|
+
const rv = data.revocationStats;
|
|
283
|
+
if (rv.revokedVCs === 0 && rv.revokedDIDs === 0 && rv.killEvents === 0) {
|
|
284
|
+
return '<div class="card"><h2>🚫 Revocation</h2><p class="empty">No revocations</p></div>';
|
|
285
|
+
}
|
|
286
|
+
return '<div class="card"><h2>🚫 Revocation Status</h2><div class="stat-grid">'
|
|
287
|
+
+ stat(rv.revokedVCs, 'Revoked VCs', 'danger')
|
|
288
|
+
+ stat(rv.revokedDIDs, 'Revoked DIDs', 'danger')
|
|
289
|
+
+ stat(rv.killEvents, 'Kill Events', 'danger')
|
|
290
|
+
+ '</div></div>';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
loadData();
|
|
294
|
+
setInterval(loadData, 5000);
|
|
295
|
+
</script>
|
|
296
|
+
</body>
|
|
297
|
+
</html>`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function escapeHtml(s) {
|
|
301
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Create and start a dashboard server.
|
|
305
|
+
*/
|
|
306
|
+
export async function createDashboard(config) {
|
|
307
|
+
const server = new DashboardServer(config);
|
|
308
|
+
await server.start();
|
|
309
|
+
return server;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Helper: build DashboardData from Sentinel components.
|
|
313
|
+
*/
|
|
314
|
+
export async function buildDashboardData(options) {
|
|
315
|
+
const auditEntries = [];
|
|
316
|
+
if (options.auditLog) {
|
|
317
|
+
const raw = await options.auditLog.readAll();
|
|
318
|
+
for (const entry of raw) {
|
|
319
|
+
auditEntries.push({
|
|
320
|
+
timestamp: entry.timestamp,
|
|
321
|
+
eventType: entry.eventType,
|
|
322
|
+
actorDid: entry.actorDid,
|
|
323
|
+
targetDid: entry.targetDid,
|
|
324
|
+
result: entry.result,
|
|
325
|
+
reason: entry.reason,
|
|
326
|
+
metadata: entry.metadata,
|
|
327
|
+
prevHash: entry.prevHash,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const revocationStats = options.revocationManager
|
|
332
|
+
? options.revocationManager.getStats()
|
|
333
|
+
: { revokedVCs: 0, revokedDIDs: 0, killEvents: 0 };
|
|
334
|
+
const offlineStats = options.offlineManager
|
|
335
|
+
? options.offlineManager.getStats()
|
|
336
|
+
: undefined;
|
|
337
|
+
return {
|
|
338
|
+
nodes: options.nodes,
|
|
339
|
+
edges: options.edges,
|
|
340
|
+
auditEntries,
|
|
341
|
+
revocationStats,
|
|
342
|
+
offlineStats,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sentinel-atl/dashboard",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Web dashboard for visualizing trust graphs, reputation scores, delegation chains, and audit logs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"lint": "tsc --noEmit",
|
|
18
|
+
"clean": "rm -rf dist",
|
|
19
|
+
"serve": "tsx src/server.ts"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@sentinel-atl/core": "*",
|
|
23
|
+
"@sentinel-atl/reputation": "*",
|
|
24
|
+
"@sentinel-atl/audit": "*",
|
|
25
|
+
"@sentinel-atl/revocation": "*",
|
|
26
|
+
"@sentinel-atl/attestation": "*",
|
|
27
|
+
"@sentinel-atl/offline": "*"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5.7.0",
|
|
31
|
+
"vitest": "^3.0.0",
|
|
32
|
+
"@types/node": "^20.0.0",
|
|
33
|
+
"tsx": "^4.7.0"
|
|
34
|
+
},
|
|
35
|
+
"license": "Apache-2.0",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/sentinel-atl/project-sentinel.git",
|
|
39
|
+
"directory": "packages/dashboard"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"ai-agent",
|
|
43
|
+
"trust",
|
|
44
|
+
"identity",
|
|
45
|
+
"did",
|
|
46
|
+
"verifiable-credentials",
|
|
47
|
+
"mcp",
|
|
48
|
+
"security"
|
|
49
|
+
],
|
|
50
|
+
"homepage": "https://github.com/sentinel-atl/project-sentinel#readme",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/sentinel-atl/project-sentinel/issues"
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"dist",
|
|
56
|
+
"README.md"
|
|
57
|
+
]
|
|
58
|
+
}
|