sensorium-mcp 2.8.52 → 2.9.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/dist/dashboard.d.ts +32 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +613 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/index.js +45 -11
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +31 -10
- package/dist/memory.js.map +1 -1
- package/dist/openai.js +1 -1
- package/dist/openai.js.map +1 -1
- package/dist/scheduler.js +1 -1
- package/dist/scheduler.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard — Beautiful web UI for monitoring sensorium-mcp agent sessions.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* GET / → Serve the SPA (single-page HTML with embedded CSS/JS)
|
|
6
|
+
* GET /api/status → Memory stats + session overview
|
|
7
|
+
* GET /api/sessions → Active MCP sessions
|
|
8
|
+
* GET /api/notes → Browse semantic notes (query params: type, limit, sort)
|
|
9
|
+
* GET /api/episodes → Recent episodes (query params: threadId, limit)
|
|
10
|
+
* GET /api/topics → Topic index
|
|
11
|
+
* GET /api/search → Search notes (query param: q)
|
|
12
|
+
*
|
|
13
|
+
* All /api/* routes require Bearer token auth (same as MCP_HTTP_SECRET).
|
|
14
|
+
* The dashboard page itself is served without auth — API token entered in the UI.
|
|
15
|
+
*/
|
|
16
|
+
import type { Database } from "better-sqlite3";
|
|
17
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
18
|
+
export interface DashboardContext {
|
|
19
|
+
getDb: () => Database;
|
|
20
|
+
getActiveSessions: () => Array<{
|
|
21
|
+
threadId: number;
|
|
22
|
+
mcpSessionId: string;
|
|
23
|
+
lastActivity: number;
|
|
24
|
+
transportType: string;
|
|
25
|
+
}>;
|
|
26
|
+
serverStartTime: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Handle a dashboard or API request. Returns true if handled, false if not a dashboard route.
|
|
30
|
+
*/
|
|
31
|
+
export declare function handleDashboardRequest(req: IncomingMessage, res: ServerResponse, ctx: DashboardContext, authToken?: string): boolean;
|
|
32
|
+
//# sourceMappingURL=dashboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAYjE,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,MAAM,QAAQ,CAAC;IACtB,iBAAiB,EAAE,MAAM,KAAK,CAAC;QAC3B,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,aAAa,EAAE,MAAM,CAAC;KACzB,CAAC,CAAC;IACH,eAAe,EAAE,MAAM,CAAC;CAC3B;AAID;;GAEG;AACH,wBAAgB,sBAAsB,CAClC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,gBAAgB,EACrB,SAAS,CAAC,EAAE,MAAM,GACnB,OAAO,CA0BT"}
|
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard — Beautiful web UI for monitoring sensorium-mcp agent sessions.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* GET / → Serve the SPA (single-page HTML with embedded CSS/JS)
|
|
6
|
+
* GET /api/status → Memory stats + session overview
|
|
7
|
+
* GET /api/sessions → Active MCP sessions
|
|
8
|
+
* GET /api/notes → Browse semantic notes (query params: type, limit, sort)
|
|
9
|
+
* GET /api/episodes → Recent episodes (query params: threadId, limit)
|
|
10
|
+
* GET /api/topics → Topic index
|
|
11
|
+
* GET /api/search → Search notes (query param: q)
|
|
12
|
+
*
|
|
13
|
+
* All /api/* routes require Bearer token auth (same as MCP_HTTP_SECRET).
|
|
14
|
+
* The dashboard page itself is served without auth — API token entered in the UI.
|
|
15
|
+
*/
|
|
16
|
+
import { getRecentEpisodes, getTopicIndex, getTopSemanticNotes, searchSemanticNotesRanked } from "./memory.js";
|
|
17
|
+
// ─── Route handler ───────────────────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Handle a dashboard or API request. Returns true if handled, false if not a dashboard route.
|
|
20
|
+
*/
|
|
21
|
+
export function handleDashboardRequest(req, res, ctx, authToken) {
|
|
22
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
23
|
+
const path = url.pathname;
|
|
24
|
+
// Serve dashboard SPA
|
|
25
|
+
if (path === "/" || path === "/dashboard") {
|
|
26
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
27
|
+
res.end(getDashboardHTML());
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
// All /api/* routes require auth
|
|
31
|
+
if (path.startsWith("/api/")) {
|
|
32
|
+
if (authToken) {
|
|
33
|
+
const auth = req.headers.authorization;
|
|
34
|
+
const providedToken = auth?.startsWith("Bearer ") ? auth.slice(7) : url.searchParams.get("token");
|
|
35
|
+
if (!providedToken || providedToken !== authToken) {
|
|
36
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
37
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return handleApiRoute(path, url, res, ctx);
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
function handleApiRoute(path, url, res, ctx) {
|
|
46
|
+
const json = (data, status = 200) => {
|
|
47
|
+
res.writeHead(status, {
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
"Cache-Control": "no-cache",
|
|
50
|
+
});
|
|
51
|
+
res.end(JSON.stringify(data));
|
|
52
|
+
};
|
|
53
|
+
try {
|
|
54
|
+
const db = ctx.getDb();
|
|
55
|
+
if (path === "/api/status") {
|
|
56
|
+
const totalEpisodes = db.prepare(`SELECT COUNT(*) as cnt FROM episodes`).get().cnt;
|
|
57
|
+
const unconsolidatedEpisodes = db.prepare(`SELECT COUNT(*) as cnt FROM episodes WHERE consolidated = 0`).get().cnt;
|
|
58
|
+
const totalSemanticNotes = db.prepare(`SELECT COUNT(*) as cnt FROM semantic_notes WHERE valid_to IS NULL AND superseded_by IS NULL`).get().cnt;
|
|
59
|
+
const totalProcedures = db.prepare(`SELECT COUNT(*) as cnt FROM procedures`).get().cnt;
|
|
60
|
+
const totalVoiceSignatures = db.prepare(`SELECT COUNT(*) as cnt FROM voice_signatures`).get().cnt;
|
|
61
|
+
const lastConso = db.prepare(`SELECT run_at FROM meta_consolidation_log ORDER BY run_at DESC LIMIT 1`).get();
|
|
62
|
+
const topTopics = getTopicIndex(db).slice(0, 10);
|
|
63
|
+
const dbSizeRow = db.prepare(`SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()`).get();
|
|
64
|
+
const sessions = ctx.getActiveSessions();
|
|
65
|
+
json({
|
|
66
|
+
memory: { totalEpisodes, unconsolidatedEpisodes, totalSemanticNotes, totalProcedures, totalVoiceSignatures, lastConsolidation: lastConso?.run_at ?? null, topTopics, dbSizeBytes: dbSizeRow?.size ?? 0 },
|
|
67
|
+
activeSessions: sessions.length,
|
|
68
|
+
sessions,
|
|
69
|
+
uptime: Math.floor((Date.now() - ctx.serverStartTime) / 1000),
|
|
70
|
+
serverTime: new Date().toISOString(),
|
|
71
|
+
});
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (path === "/api/sessions") {
|
|
75
|
+
json(ctx.getActiveSessions());
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (path === "/api/notes") {
|
|
79
|
+
const type = url.searchParams.get("type") || undefined;
|
|
80
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
|
|
81
|
+
const sort = (url.searchParams.get("sort") ?? "created_at");
|
|
82
|
+
const validTypes = ["fact", "preference", "pattern", "entity", "relationship"];
|
|
83
|
+
const notes = getTopSemanticNotes(db, {
|
|
84
|
+
type: type && validTypes.includes(type) ? type : undefined,
|
|
85
|
+
limit: Math.min(limit, 200),
|
|
86
|
+
sortBy: sort,
|
|
87
|
+
});
|
|
88
|
+
json(notes);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (path === "/api/episodes") {
|
|
92
|
+
const threadId = url.searchParams.get("threadId") ? parseInt(url.searchParams.get("threadId"), 10) : undefined;
|
|
93
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "30", 10);
|
|
94
|
+
const cappedLimit = Math.min(limit, 200);
|
|
95
|
+
if (threadId) {
|
|
96
|
+
json(getRecentEpisodes(db, threadId, cappedLimit));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const rows = db.prepare(`SELECT * FROM episodes ORDER BY timestamp DESC LIMIT ?`).all(cappedLimit);
|
|
100
|
+
json(rows.map((r) => ({
|
|
101
|
+
episodeId: r.episode_id, threadId: r.thread_id, type: r.type, modality: r.modality,
|
|
102
|
+
content: typeof r.content === "string" ? safeParseJSON(r.content) : r.content,
|
|
103
|
+
importance: r.importance, consolidated: !!r.consolidated, createdAt: r.timestamp,
|
|
104
|
+
})));
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
if (path === "/api/topics") {
|
|
109
|
+
json(getTopicIndex(db));
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (path === "/api/search") {
|
|
113
|
+
const q = url.searchParams.get("q")?.trim();
|
|
114
|
+
if (!q) {
|
|
115
|
+
json({ error: "Missing ?q= parameter" }, 400);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
json(searchSemanticNotesRanked(db, q, { maxResults: parseInt(url.searchParams.get("limit") ?? "20", 10) }));
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
json({ error: "Not found" }, 404);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function safeParseJSON(s) {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(s);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return s;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ─── Dashboard SPA HTML ──────────────────────────────────────────────────────
|
|
138
|
+
function getDashboardHTML() {
|
|
139
|
+
return `<!DOCTYPE html>
|
|
140
|
+
<html lang="en" class="dark">
|
|
141
|
+
<head>
|
|
142
|
+
<meta charset="utf-8" />
|
|
143
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
144
|
+
<title>Sensorium MCP — Dashboard</title>
|
|
145
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
146
|
+
<script>
|
|
147
|
+
tailwind.config = {
|
|
148
|
+
darkMode: 'class',
|
|
149
|
+
theme: {
|
|
150
|
+
extend: {
|
|
151
|
+
colors: {
|
|
152
|
+
surface: '#0f1419',
|
|
153
|
+
card: '#1a1f2e',
|
|
154
|
+
cardHover: '#222839',
|
|
155
|
+
accent: '#6366f1',
|
|
156
|
+
accentLight: '#818cf8',
|
|
157
|
+
success: '#22c55e',
|
|
158
|
+
warn: '#f59e0b',
|
|
159
|
+
danger: '#ef4444',
|
|
160
|
+
muted: '#6b7280',
|
|
161
|
+
textPrimary: '#e5e7eb',
|
|
162
|
+
textSecondary: '#9ca3af',
|
|
163
|
+
},
|
|
164
|
+
fontFamily: {
|
|
165
|
+
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
|
166
|
+
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
|
|
167
|
+
},
|
|
168
|
+
animation: {
|
|
169
|
+
'fade-in': 'fadeIn 0.3s ease-out',
|
|
170
|
+
'slide-up': 'slideUp 0.4s ease-out',
|
|
171
|
+
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
172
|
+
},
|
|
173
|
+
keyframes: {
|
|
174
|
+
fadeIn: { '0%': { opacity: 0 }, '100%': { opacity: 1 } },
|
|
175
|
+
slideUp: { '0%': { opacity: 0, transform: 'translateY(12px)' }, '100%': { opacity: 1, transform: 'translateY(0)' } },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
</script>
|
|
181
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
182
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
183
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
184
|
+
<style>
|
|
185
|
+
body { background: #0f1419; }
|
|
186
|
+
::-webkit-scrollbar { width: 6px; }
|
|
187
|
+
::-webkit-scrollbar-track { background: #1a1f2e; }
|
|
188
|
+
::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; }
|
|
189
|
+
::-webkit-scrollbar-thumb:hover { background: #4b5563; }
|
|
190
|
+
.glass { background: rgba(26, 31, 46, 0.8); backdrop-filter: blur(12px); border: 1px solid rgba(99, 102, 241, 0.1); }
|
|
191
|
+
.stat-glow { box-shadow: 0 0 20px rgba(99, 102, 241, 0.08); }
|
|
192
|
+
.priority-2 { border-left: 3px solid #ef4444; }
|
|
193
|
+
.priority-1 { border-left: 3px solid #f59e0b; }
|
|
194
|
+
.priority-0 { border-left: 3px solid transparent; }
|
|
195
|
+
.type-badge { font-size: 0.65rem; padding: 2px 6px; border-radius: 9999px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
196
|
+
.type-fact { background: rgba(59, 130, 246, 0.15); color: #60a5fa; }
|
|
197
|
+
.type-preference { background: rgba(168, 85, 247, 0.15); color: #c084fc; }
|
|
198
|
+
.type-pattern { background: rgba(34, 197, 94, 0.15); color: #4ade80; }
|
|
199
|
+
.type-entity { background: rgba(251, 191, 36, 0.15); color: #fbbf24; }
|
|
200
|
+
.type-relationship { background: rgba(244, 114, 182, 0.15); color: #f472b6; }
|
|
201
|
+
.tab-active { border-bottom: 2px solid #6366f1; color: #e5e7eb; }
|
|
202
|
+
.tab-inactive { border-bottom: 2px solid transparent; color: #6b7280; }
|
|
203
|
+
.tab-inactive:hover { color: #9ca3af; }
|
|
204
|
+
</style>
|
|
205
|
+
</head>
|
|
206
|
+
<body class="font-sans text-textPrimary min-h-screen">
|
|
207
|
+
<!-- Auth overlay -->
|
|
208
|
+
<div id="auth-overlay" class="fixed inset-0 z-50 flex items-center justify-center bg-surface/95 backdrop-blur-sm">
|
|
209
|
+
<div class="glass rounded-2xl p-8 max-w-md w-full mx-4 animate-slide-up">
|
|
210
|
+
<div class="flex items-center gap-3 mb-6">
|
|
211
|
+
<div class="w-10 h-10 rounded-xl bg-accent/20 flex items-center justify-center">
|
|
212
|
+
<svg class="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
213
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
|
214
|
+
</svg>
|
|
215
|
+
</div>
|
|
216
|
+
<div>
|
|
217
|
+
<h2 class="text-lg font-semibold">Sensorium MCP</h2>
|
|
218
|
+
<p class="text-sm text-textSecondary">Enter your API token</p>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
<input id="token-input" type="password" placeholder="MCP_HTTP_SECRET"
|
|
222
|
+
class="w-full px-4 py-3 rounded-xl bg-surface border border-gray-700 text-textPrimary placeholder-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition font-mono text-sm" />
|
|
223
|
+
<button onclick="authenticate()" class="w-full mt-4 px-4 py-3 rounded-xl bg-accent hover:bg-accentLight text-white font-medium transition">
|
|
224
|
+
Connect
|
|
225
|
+
</button>
|
|
226
|
+
<p id="auth-error" class="mt-3 text-sm text-danger hidden">Invalid token</p>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<!-- Main dashboard -->
|
|
231
|
+
<div id="dashboard" class="hidden">
|
|
232
|
+
<!-- Header -->
|
|
233
|
+
<header class="glass sticky top-0 z-40 border-b border-gray-800/50">
|
|
234
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 py-4 flex items-center justify-between">
|
|
235
|
+
<div class="flex items-center gap-3">
|
|
236
|
+
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-accent to-purple-500 flex items-center justify-center">
|
|
237
|
+
<span class="text-white text-sm font-bold">S</span>
|
|
238
|
+
</div>
|
|
239
|
+
<div>
|
|
240
|
+
<h1 class="text-lg font-semibold tracking-tight">Sensorium MCP</h1>
|
|
241
|
+
<p class="text-xs text-textSecondary">Agent Dashboard</p>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="flex items-center gap-4">
|
|
245
|
+
<div id="connection-status" class="flex items-center gap-2 text-sm">
|
|
246
|
+
<span class="w-2 h-2 rounded-full bg-success animate-pulse-slow"></span>
|
|
247
|
+
<span class="text-textSecondary">Connected</span>
|
|
248
|
+
</div>
|
|
249
|
+
<div id="uptime-display" class="text-sm text-textSecondary font-mono"></div>
|
|
250
|
+
<button onclick="logout()" class="text-sm text-muted hover:text-textSecondary transition">Disconnect</button>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</header>
|
|
254
|
+
|
|
255
|
+
<!-- Stats bar -->
|
|
256
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 py-6">
|
|
257
|
+
<div id="stats-grid" class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
|
|
258
|
+
<!-- Filled by JS -->
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<!-- Tabs -->
|
|
263
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6">
|
|
264
|
+
<nav class="flex gap-6 border-b border-gray-800/50 mb-6">
|
|
265
|
+
<button onclick="switchTab('sessions')" id="tab-sessions" class="pb-3 text-sm font-medium tab-active transition">Sessions</button>
|
|
266
|
+
<button onclick="switchTab('notes')" id="tab-notes" class="pb-3 text-sm font-medium tab-inactive transition">Memory Notes</button>
|
|
267
|
+
<button onclick="switchTab('episodes')" id="tab-episodes" class="pb-3 text-sm font-medium tab-inactive transition">Episodes</button>
|
|
268
|
+
<button onclick="switchTab('topics')" id="tab-topics" class="pb-3 text-sm font-medium tab-inactive transition">Topics</button>
|
|
269
|
+
</nav>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<!-- Tab content -->
|
|
273
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 pb-12">
|
|
274
|
+
<!-- Sessions -->
|
|
275
|
+
<div id="panel-sessions" class="animate-fade-in">
|
|
276
|
+
<div id="sessions-list" class="space-y-3"></div>
|
|
277
|
+
<p id="sessions-empty" class="hidden text-center text-textSecondary py-12">No active sessions</p>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<!-- Notes -->
|
|
281
|
+
<div id="panel-notes" class="hidden animate-fade-in">
|
|
282
|
+
<div class="flex flex-wrap items-center gap-3 mb-4">
|
|
283
|
+
<input id="notes-search" type="text" placeholder="Search notes..."
|
|
284
|
+
class="flex-1 min-w-[200px] px-4 py-2 rounded-xl bg-card border border-gray-700 text-sm text-textPrimary placeholder-muted focus:outline-none focus:border-accent transition" />
|
|
285
|
+
<select id="notes-type" onchange="loadNotes()"
|
|
286
|
+
class="px-3 py-2 rounded-xl bg-card border border-gray-700 text-sm text-textPrimary focus:outline-none">
|
|
287
|
+
<option value="">All types</option>
|
|
288
|
+
<option value="fact">Facts</option>
|
|
289
|
+
<option value="preference">Preferences</option>
|
|
290
|
+
<option value="pattern">Patterns</option>
|
|
291
|
+
<option value="entity">Entities</option>
|
|
292
|
+
<option value="relationship">Relationships</option>
|
|
293
|
+
</select>
|
|
294
|
+
<select id="notes-sort" onchange="loadNotes()"
|
|
295
|
+
class="px-3 py-2 rounded-xl bg-card border border-gray-700 text-sm text-textPrimary focus:outline-none">
|
|
296
|
+
<option value="created_at">Newest</option>
|
|
297
|
+
<option value="confidence">Confidence</option>
|
|
298
|
+
<option value="access_count">Most accessed</option>
|
|
299
|
+
</select>
|
|
300
|
+
</div>
|
|
301
|
+
<div id="notes-list" class="space-y-2"></div>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<!-- Episodes -->
|
|
305
|
+
<div id="panel-episodes" class="hidden animate-fade-in">
|
|
306
|
+
<div class="flex items-center gap-3 mb-4">
|
|
307
|
+
<input id="episodes-thread" type="number" placeholder="Thread ID (optional)"
|
|
308
|
+
class="w-48 px-4 py-2 rounded-xl bg-card border border-gray-700 text-sm text-textPrimary placeholder-muted focus:outline-none focus:border-accent transition" />
|
|
309
|
+
<button onclick="loadEpisodes()" class="px-4 py-2 rounded-xl bg-accent hover:bg-accentLight text-white text-sm font-medium transition">Load</button>
|
|
310
|
+
</div>
|
|
311
|
+
<div id="episodes-list" class="space-y-2"></div>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<!-- Topics -->
|
|
315
|
+
<div id="panel-topics" class="hidden animate-fade-in">
|
|
316
|
+
<div id="topics-grid" class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3"></div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<script>
|
|
322
|
+
// ─── State ─────────────────────────────────────────────────────────
|
|
323
|
+
let token = localStorage.getItem('sensorium_token') || '';
|
|
324
|
+
let currentTab = 'sessions';
|
|
325
|
+
let refreshTimer = null;
|
|
326
|
+
|
|
327
|
+
// ─── Auth ──────────────────────────────────────────────────────────
|
|
328
|
+
async function authenticate() {
|
|
329
|
+
const input = document.getElementById('token-input');
|
|
330
|
+
token = input.value.trim();
|
|
331
|
+
if (!token) { token = 'no-auth'; } // allow no-auth mode
|
|
332
|
+
try {
|
|
333
|
+
const res = await api('/api/status');
|
|
334
|
+
if (res) {
|
|
335
|
+
localStorage.setItem('sensorium_token', token);
|
|
336
|
+
document.getElementById('auth-overlay').classList.add('hidden');
|
|
337
|
+
document.getElementById('dashboard').classList.remove('hidden');
|
|
338
|
+
startRefresh();
|
|
339
|
+
}
|
|
340
|
+
} catch (e) {
|
|
341
|
+
document.getElementById('auth-error').classList.remove('hidden');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function logout() {
|
|
346
|
+
localStorage.removeItem('sensorium_token');
|
|
347
|
+
token = '';
|
|
348
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
349
|
+
document.getElementById('dashboard').classList.add('hidden');
|
|
350
|
+
document.getElementById('auth-overlay').classList.remove('hidden');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Auto-connect if token saved
|
|
354
|
+
if (token) {
|
|
355
|
+
api('/api/status').then(data => {
|
|
356
|
+
if (data) {
|
|
357
|
+
document.getElementById('auth-overlay').classList.add('hidden');
|
|
358
|
+
document.getElementById('dashboard').classList.remove('hidden');
|
|
359
|
+
startRefresh();
|
|
360
|
+
}
|
|
361
|
+
}).catch(() => {
|
|
362
|
+
localStorage.removeItem('sensorium_token');
|
|
363
|
+
token = '';
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Enter key on token input
|
|
368
|
+
document.getElementById('token-input').addEventListener('keydown', e => {
|
|
369
|
+
if (e.key === 'Enter') authenticate();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// ─── API ───────────────────────────────────────────────────────────
|
|
373
|
+
async function api(path) {
|
|
374
|
+
const res = await fetch(path, {
|
|
375
|
+
headers: { 'Authorization': 'Bearer ' + token },
|
|
376
|
+
});
|
|
377
|
+
if (!res.ok) throw new Error(res.statusText);
|
|
378
|
+
return res.json();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ─── Rendering ─────────────────────────────────────────────────────
|
|
382
|
+
function formatUptime(seconds) {
|
|
383
|
+
const d = Math.floor(seconds / 86400);
|
|
384
|
+
const h = Math.floor((seconds % 86400) / 3600);
|
|
385
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
386
|
+
if (d > 0) return d + 'd ' + h + 'h';
|
|
387
|
+
if (h > 0) return h + 'h ' + m + 'm';
|
|
388
|
+
return m + 'm';
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function timeAgo(iso) {
|
|
392
|
+
if (!iso) return 'never';
|
|
393
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
394
|
+
const mins = Math.floor(diff / 60000);
|
|
395
|
+
if (mins < 1) return 'just now';
|
|
396
|
+
if (mins < 60) return mins + 'm ago';
|
|
397
|
+
const hours = Math.floor(mins / 60);
|
|
398
|
+
if (hours < 24) return hours + 'h ago';
|
|
399
|
+
return Math.floor(hours / 24) + 'd ago';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function statCard(label, value, icon, color = 'accent') {
|
|
403
|
+
return '<div class="glass rounded-xl p-4 stat-glow animate-slide-up">' +
|
|
404
|
+
'<div class="flex items-center gap-2 mb-2">' +
|
|
405
|
+
'<span class="text-' + color + '">' + icon + '</span>' +
|
|
406
|
+
'<span class="text-xs text-textSecondary font-medium uppercase tracking-wider">' + label + '</span>' +
|
|
407
|
+
'</div>' +
|
|
408
|
+
'<div class="text-2xl font-bold font-mono">' + value + '</div>' +
|
|
409
|
+
'</div>';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function renderStats(data) {
|
|
413
|
+
const m = data.memory;
|
|
414
|
+
const grid = document.getElementById('stats-grid');
|
|
415
|
+
grid.innerHTML =
|
|
416
|
+
statCard('Sessions', data.activeSessions,
|
|
417
|
+
'<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>',
|
|
418
|
+
'success') +
|
|
419
|
+
statCard('Notes', m.totalSemanticNotes,
|
|
420
|
+
'<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>',
|
|
421
|
+
'accentLight') +
|
|
422
|
+
statCard('Episodes', m.totalEpisodes,
|
|
423
|
+
'<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>',
|
|
424
|
+
'warn') +
|
|
425
|
+
statCard('Unconsolidated', m.unconsolidatedEpisodes,
|
|
426
|
+
'<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>',
|
|
427
|
+
m.unconsolidatedEpisodes > 10 ? 'danger' : 'success') +
|
|
428
|
+
statCard('Procedures', m.totalProcedures,
|
|
429
|
+
'<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>') +
|
|
430
|
+
statCard('Uptime', formatUptime(data.uptime),
|
|
431
|
+
'<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>',
|
|
432
|
+
'success');
|
|
433
|
+
|
|
434
|
+
document.getElementById('uptime-display').textContent = formatUptime(data.uptime);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function renderSessions(sessions) {
|
|
438
|
+
const list = document.getElementById('sessions-list');
|
|
439
|
+
const empty = document.getElementById('sessions-empty');
|
|
440
|
+
if (!sessions.length) {
|
|
441
|
+
list.innerHTML = '';
|
|
442
|
+
empty.classList.remove('hidden');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
empty.classList.add('hidden');
|
|
446
|
+
list.innerHTML = sessions.map(s => {
|
|
447
|
+
const idle = Math.floor((Date.now() - s.lastActivity) / 60000);
|
|
448
|
+
const statusColor = idle < 5 ? 'success' : idle < 30 ? 'warn' : 'danger';
|
|
449
|
+
const statusLabel = idle < 5 ? 'Active' : idle < 30 ? 'Idle ' + idle + 'm' : 'Dormant ' + idle + 'm';
|
|
450
|
+
return '<div class="glass rounded-xl p-4 animate-slide-up">' +
|
|
451
|
+
'<div class="flex items-center justify-between">' +
|
|
452
|
+
'<div class="flex items-center gap-3">' +
|
|
453
|
+
'<span class="w-2.5 h-2.5 rounded-full bg-' + statusColor + '"></span>' +
|
|
454
|
+
'<div>' +
|
|
455
|
+
'<div class="font-medium">Thread ' + s.threadId + '</div>' +
|
|
456
|
+
'<div class="text-xs text-textSecondary font-mono">' + s.mcpSessionId.slice(0, 12) + '...</div>' +
|
|
457
|
+
'</div>' +
|
|
458
|
+
'</div>' +
|
|
459
|
+
'<div class="text-right">' +
|
|
460
|
+
'<div class="text-sm font-medium text-' + statusColor + '">' + statusLabel + '</div>' +
|
|
461
|
+
'<div class="text-xs text-textSecondary">' + s.transportType + '</div>' +
|
|
462
|
+
'</div>' +
|
|
463
|
+
'</div>' +
|
|
464
|
+
'</div>';
|
|
465
|
+
}).join('');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function renderNotes(notes) {
|
|
469
|
+
const list = document.getElementById('notes-list');
|
|
470
|
+
list.innerHTML = notes.map(n => {
|
|
471
|
+
const pClass = 'priority-' + (n.priority || 0);
|
|
472
|
+
return '<div class="glass rounded-xl p-4 ' + pClass + ' animate-fade-in">' +
|
|
473
|
+
'<div class="flex items-start justify-between gap-3">' +
|
|
474
|
+
'<div class="flex-1 min-w-0">' +
|
|
475
|
+
'<div class="flex items-center gap-2 mb-1">' +
|
|
476
|
+
'<span class="type-badge type-' + n.type + '">' + n.type + '</span>' +
|
|
477
|
+
(n.priority >= 2 ? '<span class="type-badge" style="background:rgba(239,68,68,0.15);color:#f87171">HIGH IMPORTANCE</span>' : '') +
|
|
478
|
+
(n.priority === 1 ? '<span class="type-badge" style="background:rgba(245,158,11,0.15);color:#fbbf24">NOTABLE</span>' : '') +
|
|
479
|
+
'<span class="text-xs text-textSecondary">' + n.noteId + '</span>' +
|
|
480
|
+
'</div>' +
|
|
481
|
+
'<p class="text-sm text-textPrimary leading-relaxed">' + escapeHtml(n.content) + '</p>' +
|
|
482
|
+
'<div class="flex flex-wrap gap-1.5 mt-2">' +
|
|
483
|
+
(n.keywords || []).map(k => '<span class="text-xs px-2 py-0.5 rounded-full bg-accent/10 text-accentLight">' + escapeHtml(k) + '</span>').join('') +
|
|
484
|
+
'</div>' +
|
|
485
|
+
'</div>' +
|
|
486
|
+
'<div class="text-right shrink-0">' +
|
|
487
|
+
'<div class="text-sm font-mono text-textSecondary">' + (n.confidence * 100).toFixed(0) + '%</div>' +
|
|
488
|
+
'<div class="text-xs text-muted">' + timeAgo(n.createdAt) + '</div>' +
|
|
489
|
+
'<div class="text-xs text-muted">' + n.accessCount + ' hits</div>' +
|
|
490
|
+
'</div>' +
|
|
491
|
+
'</div>' +
|
|
492
|
+
'</div>';
|
|
493
|
+
}).join('');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function renderEpisodes(episodes) {
|
|
497
|
+
const list = document.getElementById('episodes-list');
|
|
498
|
+
const modalityIcons = {
|
|
499
|
+
text: '💬', voice: '🎤', image: '🖼️', file: '📎', system: '⚙️'
|
|
500
|
+
};
|
|
501
|
+
list.innerHTML = episodes.map(ep => {
|
|
502
|
+
const icon = modalityIcons[ep.modality] || '📝';
|
|
503
|
+
const content = ep.content ? (typeof ep.content === 'object' ? JSON.stringify(ep.content).slice(0, 300) : String(ep.content).slice(0, 300)) : '(no content)';
|
|
504
|
+
return '<div class="glass rounded-xl p-4 animate-fade-in">' +
|
|
505
|
+
'<div class="flex items-start gap-3">' +
|
|
506
|
+
'<span class="text-lg">' + icon + '</span>' +
|
|
507
|
+
'<div class="flex-1 min-w-0">' +
|
|
508
|
+
'<div class="flex items-center gap-2 mb-1">' +
|
|
509
|
+
'<span class="type-badge type-fact">' + ep.type + '</span>' +
|
|
510
|
+
'<span class="text-xs text-textSecondary font-mono">' + ep.episodeId + '</span>' +
|
|
511
|
+
'<span class="text-xs text-muted">' + timeAgo(ep.createdAt) + '</span>' +
|
|
512
|
+
'</div>' +
|
|
513
|
+
'<p class="text-sm text-textSecondary leading-relaxed break-words">' + escapeHtml(content) + '</p>' +
|
|
514
|
+
'</div>' +
|
|
515
|
+
'<div class="text-xs text-muted shrink-0">imp: ' + ((ep.importance || 0) * 100).toFixed(0) + '%</div>' +
|
|
516
|
+
'</div>' +
|
|
517
|
+
'</div>';
|
|
518
|
+
}).join('');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function renderTopics(topics) {
|
|
522
|
+
const grid = document.getElementById('topics-grid');
|
|
523
|
+
if (!topics.length) { grid.innerHTML = '<p class="text-textSecondary col-span-full text-center py-12">No topics yet</p>'; return; }
|
|
524
|
+
const maxCount = Math.max(...topics.map(t => t.count));
|
|
525
|
+
grid.innerHTML = topics.map(t => {
|
|
526
|
+
const intensity = Math.max(0.15, t.count / maxCount);
|
|
527
|
+
return '<div class="glass rounded-xl p-4 animate-slide-up" style="border-left: 3px solid rgba(99,102,241,' + intensity + ')">' +
|
|
528
|
+
'<div class="font-medium text-sm">' + escapeHtml(t.topic) + '</div>' +
|
|
529
|
+
'<div class="flex items-center justify-between mt-2">' +
|
|
530
|
+
'<span class="text-lg font-bold font-mono text-accent">' + t.count + '</span>' +
|
|
531
|
+
'<span class="text-xs text-muted">' + timeAgo(t.lastSeen) + '</span>' +
|
|
532
|
+
'</div>' +
|
|
533
|
+
'</div>';
|
|
534
|
+
}).join('');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ─── Tab switching ──────────────────────────────────────────────────
|
|
538
|
+
function switchTab(tab) {
|
|
539
|
+
const tabs = ['sessions', 'notes', 'episodes', 'topics'];
|
|
540
|
+
tabs.forEach(t => {
|
|
541
|
+
document.getElementById('panel-' + t).classList.toggle('hidden', t !== tab);
|
|
542
|
+
document.getElementById('tab-' + t).className = 'pb-3 text-sm font-medium transition ' + (t === tab ? 'tab-active' : 'tab-inactive');
|
|
543
|
+
});
|
|
544
|
+
currentTab = tab;
|
|
545
|
+
refreshCurrentTab();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ─── Data loading ─────────────────────────────────────────────────
|
|
549
|
+
let searchDebounce = null;
|
|
550
|
+
document.getElementById('notes-search')?.addEventListener('input', () => {
|
|
551
|
+
clearTimeout(searchDebounce);
|
|
552
|
+
searchDebounce = setTimeout(() => loadNotes(), 300);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
async function loadNotes() {
|
|
556
|
+
try {
|
|
557
|
+
const q = document.getElementById('notes-search').value.trim();
|
|
558
|
+
const type = document.getElementById('notes-type').value;
|
|
559
|
+
const sort = document.getElementById('notes-sort').value;
|
|
560
|
+
let notes;
|
|
561
|
+
if (q) {
|
|
562
|
+
notes = await api('/api/search?q=' + encodeURIComponent(q) + '&limit=50');
|
|
563
|
+
} else {
|
|
564
|
+
notes = await api('/api/notes?limit=50' + (type ? '&type=' + type : '') + '&sort=' + sort);
|
|
565
|
+
}
|
|
566
|
+
renderNotes(notes);
|
|
567
|
+
} catch (e) { console.error('Notes load error:', e); }
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function loadEpisodes() {
|
|
571
|
+
try {
|
|
572
|
+
const threadId = document.getElementById('episodes-thread').value;
|
|
573
|
+
let url = '/api/episodes?limit=30';
|
|
574
|
+
if (threadId) url += '&threadId=' + threadId;
|
|
575
|
+
const episodes = await api(url);
|
|
576
|
+
renderEpisodes(episodes);
|
|
577
|
+
} catch (e) { console.error('Episodes load error:', e); }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function loadTopics() {
|
|
581
|
+
try {
|
|
582
|
+
const topics = await api('/api/topics');
|
|
583
|
+
renderTopics(topics);
|
|
584
|
+
} catch (e) { console.error('Topics load error:', e); }
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function refreshCurrentTab() {
|
|
588
|
+
const data = await api('/api/status').catch(() => null);
|
|
589
|
+
if (data) {
|
|
590
|
+
renderStats(data);
|
|
591
|
+
renderSessions(data.sessions);
|
|
592
|
+
}
|
|
593
|
+
if (currentTab === 'notes') loadNotes();
|
|
594
|
+
if (currentTab === 'episodes') loadEpisodes();
|
|
595
|
+
if (currentTab === 'topics') loadTopics();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function startRefresh() {
|
|
599
|
+
refreshCurrentTab();
|
|
600
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
601
|
+
refreshTimer = setInterval(refreshCurrentTab, 5000);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ─── Utilities ──────────────────────────────────────────────────────
|
|
605
|
+
function escapeHtml(str) {
|
|
606
|
+
if (!str) return '';
|
|
607
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
608
|
+
}
|
|
609
|
+
</script>
|
|
610
|
+
</body>
|
|
611
|
+
</html>`;
|
|
612
|
+
}
|
|
613
|
+
//# sourceMappingURL=dashboard.js.map
|