sensorium-mcp 2.16.35 → 2.16.36
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.
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Sensorium MCP — Dashboard</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script>
|
|
9
|
+
tailwind.config = {
|
|
10
|
+
darkMode: 'class',
|
|
11
|
+
theme: {
|
|
12
|
+
extend: {
|
|
13
|
+
colors: {
|
|
14
|
+
surface: '#0f1419',
|
|
15
|
+
card: '#1a1f2e',
|
|
16
|
+
cardHover: '#222839',
|
|
17
|
+
accent: '#6366f1',
|
|
18
|
+
accentLight: '#818cf8',
|
|
19
|
+
success: '#22c55e',
|
|
20
|
+
warn: '#f59e0b',
|
|
21
|
+
danger: '#ef4444',
|
|
22
|
+
muted: '#6b7280',
|
|
23
|
+
textPrimary: '#e5e7eb',
|
|
24
|
+
textSecondary: '#9ca3af',
|
|
25
|
+
},
|
|
26
|
+
fontFamily: {
|
|
27
|
+
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
|
28
|
+
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
|
|
29
|
+
},
|
|
30
|
+
animation: {
|
|
31
|
+
'fade-in': 'fadeIn 0.3s ease-out',
|
|
32
|
+
'slide-up': 'slideUp 0.4s ease-out',
|
|
33
|
+
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
34
|
+
},
|
|
35
|
+
keyframes: {
|
|
36
|
+
fadeIn: { '0%': { opacity: 0 }, '100%': { opacity: 1 } },
|
|
37
|
+
slideUp: { '0%': { opacity: 0, transform: 'translateY(12px)' }, '100%': { opacity: 1, transform: 'translateY(0)' } },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
</script>
|
|
43
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
44
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
45
|
+
<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" />
|
|
46
|
+
<style>
|
|
47
|
+
body { background: #0f1419; }
|
|
48
|
+
::-webkit-scrollbar { width: 6px; }
|
|
49
|
+
::-webkit-scrollbar-track { background: #1a1f2e; }
|
|
50
|
+
::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; }
|
|
51
|
+
::-webkit-scrollbar-thumb:hover { background: #4b5563; }
|
|
52
|
+
.glass { background: rgba(26, 31, 46, 0.8); backdrop-filter: blur(12px); border: 1px solid rgba(99, 102, 241, 0.1); }
|
|
53
|
+
.stat-glow { box-shadow: 0 0 20px rgba(99, 102, 241, 0.08); }
|
|
54
|
+
.priority-2 { border-left: 3px solid #ef4444; }
|
|
55
|
+
.priority-1 { border-left: 3px solid #f59e0b; }
|
|
56
|
+
.priority-0 { border-left: 3px solid transparent; }
|
|
57
|
+
.type-badge { font-size: 0.65rem; padding: 2px 6px; border-radius: 9999px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
58
|
+
.type-fact { background: rgba(59, 130, 246, 0.15); color: #60a5fa; }
|
|
59
|
+
.type-preference { background: rgba(168, 85, 247, 0.15); color: #c084fc; }
|
|
60
|
+
.type-pattern { background: rgba(34, 197, 94, 0.15); color: #4ade80; }
|
|
61
|
+
.type-entity { background: rgba(251, 191, 36, 0.15); color: #fbbf24; }
|
|
62
|
+
.type-relationship { background: rgba(244, 114, 182, 0.15); color: #f472b6; }
|
|
63
|
+
.tab-active { border-bottom: 2px solid #6366f1; color: #e5e7eb; }
|
|
64
|
+
.tab-inactive { border-bottom: 2px solid transparent; color: #6b7280; }
|
|
65
|
+
.tab-inactive:hover { color: #9ca3af; }
|
|
66
|
+
</style>
|
|
67
|
+
</head>
|
|
68
|
+
<body class="font-sans text-textPrimary min-h-screen">
|
|
69
|
+
<!-- Auth overlay -->
|
|
70
|
+
<div id="auth-overlay" class="fixed inset-0 z-50 flex items-center justify-center bg-surface/95 backdrop-blur-sm">
|
|
71
|
+
<div class="glass rounded-2xl p-8 max-w-md w-full mx-4 animate-slide-up">
|
|
72
|
+
<div class="flex items-center gap-3 mb-6">
|
|
73
|
+
<div class="w-10 h-10 rounded-xl bg-accent/20 flex items-center justify-center">
|
|
74
|
+
<svg class="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
75
|
+
<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"/>
|
|
76
|
+
</svg>
|
|
77
|
+
</div>
|
|
78
|
+
<div>
|
|
79
|
+
<h2 class="text-lg font-semibold">Sensorium MCP</h2>
|
|
80
|
+
<p class="text-sm text-textSecondary">Enter your API token</p>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<input id="token-input" type="password" placeholder="MCP_HTTP_SECRET"
|
|
84
|
+
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" />
|
|
85
|
+
<button onclick="authenticate()" class="w-full mt-4 px-4 py-3 rounded-xl bg-accent hover:bg-accentLight text-white font-medium transition">
|
|
86
|
+
Connect
|
|
87
|
+
</button>
|
|
88
|
+
<p id="auth-error" class="mt-3 text-sm text-danger hidden">Invalid token</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- Main dashboard -->
|
|
93
|
+
<div id="dashboard" class="hidden">
|
|
94
|
+
<!-- Header -->
|
|
95
|
+
<header class="glass sticky top-0 z-40 border-b border-gray-800/50">
|
|
96
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 py-4 flex items-center justify-between">
|
|
97
|
+
<div class="flex items-center gap-3">
|
|
98
|
+
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-accent to-purple-500 flex items-center justify-center">
|
|
99
|
+
<span class="text-white text-sm font-bold">S</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div>
|
|
102
|
+
<h1 class="text-lg font-semibold tracking-tight">Sensorium MCP</h1>
|
|
103
|
+
<p class="text-xs text-textSecondary">Agent Dashboard</p>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="flex items-center gap-4">
|
|
107
|
+
<div id="connection-status" class="flex items-center gap-2 text-sm">
|
|
108
|
+
<span class="w-2 h-2 rounded-full bg-success animate-pulse-slow"></span>
|
|
109
|
+
<span class="text-textSecondary">Connected</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div id="uptime-display" class="text-sm text-textSecondary font-mono"></div>
|
|
112
|
+
<button onclick="logout()" class="text-sm text-muted hover:text-textSecondary transition">Disconnect</button>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</header>
|
|
116
|
+
|
|
117
|
+
<!-- Stats bar -->
|
|
118
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 py-6">
|
|
119
|
+
<div id="stats-grid" class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
|
|
120
|
+
<!-- Filled by JS -->
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- Tabs -->
|
|
125
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6">
|
|
126
|
+
<nav class="flex gap-6 border-b border-gray-800/50 mb-6">
|
|
127
|
+
<button onclick="switchTab('sessions')" id="tab-sessions" class="pb-3 text-sm font-medium tab-active transition">Sessions</button>
|
|
128
|
+
<button onclick="switchTab('notes')" id="tab-notes" class="pb-3 text-sm font-medium tab-inactive transition">Memory Notes</button>
|
|
129
|
+
<button onclick="switchTab('episodes')" id="tab-episodes" class="pb-3 text-sm font-medium tab-inactive transition">Episodes</button>
|
|
130
|
+
<button onclick="switchTab('topics')" id="tab-topics" class="pb-3 text-sm font-medium tab-inactive transition">Topics</button>
|
|
131
|
+
<button onclick="switchTab('templates')" id="tab-templates" class="pb-3 text-sm font-medium tab-inactive transition">Templates</button>
|
|
132
|
+
</nav>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<!-- Tab content -->
|
|
136
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 pb-12">
|
|
137
|
+
<!-- Sessions -->
|
|
138
|
+
<div id="panel-sessions" class="animate-fade-in">
|
|
139
|
+
<div id="sessions-list" class="space-y-3"></div>
|
|
140
|
+
<p id="sessions-empty" class="hidden text-center text-textSecondary py-12">No sessions</p>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- Notes -->
|
|
144
|
+
<div id="panel-notes" class="hidden animate-fade-in">
|
|
145
|
+
<div class="flex flex-wrap items-center gap-3 mb-4">
|
|
146
|
+
<input id="notes-search" type="text" placeholder="Search notes..."
|
|
147
|
+
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" />
|
|
148
|
+
<select id="notes-type" onchange="loadNotes()"
|
|
149
|
+
class="px-3 py-2 rounded-xl bg-card border border-gray-700 text-sm text-textPrimary focus:outline-none">
|
|
150
|
+
<option value="">All types</option>
|
|
151
|
+
<option value="fact">Facts</option>
|
|
152
|
+
<option value="preference">Preferences</option>
|
|
153
|
+
<option value="pattern">Patterns</option>
|
|
154
|
+
<option value="entity">Entities</option>
|
|
155
|
+
<option value="relationship">Relationships</option>
|
|
156
|
+
</select>
|
|
157
|
+
<select id="notes-sort" onchange="loadNotes()"
|
|
158
|
+
class="px-3 py-2 rounded-xl bg-card border border-gray-700 text-sm text-textPrimary focus:outline-none">
|
|
159
|
+
<option value="created_at">Newest</option>
|
|
160
|
+
<option value="confidence">Confidence</option>
|
|
161
|
+
<option value="access_count">Most accessed</option>
|
|
162
|
+
</select>
|
|
163
|
+
</div>
|
|
164
|
+
<div id="notes-list" class="space-y-2"></div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- Episodes -->
|
|
168
|
+
<div id="panel-episodes" class="hidden animate-fade-in">
|
|
169
|
+
<div class="flex items-center gap-3 mb-4">
|
|
170
|
+
<input id="episodes-thread" type="number" placeholder="Thread ID (optional)"
|
|
171
|
+
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" />
|
|
172
|
+
<button onclick="loadEpisodes()" class="px-4 py-2 rounded-xl bg-accent hover:bg-accentLight text-white text-sm font-medium transition">Load</button>
|
|
173
|
+
</div>
|
|
174
|
+
<div id="episodes-list" class="space-y-2"></div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<!-- Topics -->
|
|
178
|
+
<div id="panel-topics" class="hidden animate-fade-in">
|
|
179
|
+
<div id="topics-grid" class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3"></div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Templates -->
|
|
183
|
+
<div id="panel-templates" class="hidden animate-fade-in">
|
|
184
|
+
<div class="glass rounded-xl p-6">
|
|
185
|
+
<div class="flex flex-wrap items-center justify-between gap-4 mb-4">
|
|
186
|
+
<div>
|
|
187
|
+
<h3 class="text-lg font-semibold">Reminders Template</h3>
|
|
188
|
+
<p class="text-sm text-textSecondary mt-1">Edit the system prompt template sent with every reminder</p>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="flex items-center gap-2">
|
|
191
|
+
<span id="tpl-status" class="text-sm"></span>
|
|
192
|
+
<button onclick="resetTemplate()" class="px-4 py-2 rounded-xl bg-card hover:bg-cardHover border border-gray-700 text-sm text-textSecondary hover:text-textPrimary transition">Reset to Default</button>
|
|
193
|
+
<button onclick="saveTemplate()" class="px-4 py-2 rounded-xl bg-accent hover:bg-accentLight text-white text-sm font-medium transition">Save</button>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<div id="tpl-default-badge" class="hidden mb-3">
|
|
197
|
+
<span class="type-badge" style="background:rgba(245,158,11,0.15);color:#fbbf24">USING DEFAULT — edit and save to customize</span>
|
|
198
|
+
</div>
|
|
199
|
+
<textarea id="tpl-editor" rows="20" spellcheck="false"
|
|
200
|
+
class="w-full px-4 py-3 rounded-xl bg-surface border border-gray-700 text-textPrimary font-mono text-sm leading-relaxed focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition resize-y"
|
|
201
|
+
placeholder="Loading..."></textarea>
|
|
202
|
+
<div class="mt-4">
|
|
203
|
+
<div id="tpl-preview-header" class="flex items-center gap-2 mb-2 cursor-pointer select-none" onclick="toggleTplPreview()">
|
|
204
|
+
<svg id="tpl-preview-arrow" class="w-4 h-4 text-textSecondary transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
205
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
206
|
+
</svg>
|
|
207
|
+
<span class="text-sm font-medium text-textSecondary">Preview with highlighted variables</span>
|
|
208
|
+
</div>
|
|
209
|
+
<div id="tpl-preview" class="hidden glass rounded-xl p-4 font-mono text-sm leading-relaxed whitespace-pre-wrap break-words"></div>
|
|
210
|
+
</div>
|
|
211
|
+
<details class="mt-4">
|
|
212
|
+
<summary class="text-sm font-medium text-textSecondary cursor-pointer hover:text-textPrimary transition">Available Variables</summary>
|
|
213
|
+
<div class="mt-2 glass rounded-xl p-4 grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
|
|
214
|
+
<div><code class="text-accentLight">{{OPERATOR_MESSAGE}}</code> <span class="text-textSecondary">— latest operator message</span></div>
|
|
215
|
+
<div><code class="text-accentLight">{{THREAD_ID}}</code> <span class="text-textSecondary">— Telegram thread ID</span></div>
|
|
216
|
+
<div><code class="text-accentLight">{{TIME}}</code> <span class="text-textSecondary">— formatted timestamp</span></div>
|
|
217
|
+
<div><code class="text-accentLight">{{UPTIME}}</code> <span class="text-textSecondary">— session uptime</span></div>
|
|
218
|
+
<div><code class="text-accentLight">{{VERSION}}</code> <span class="text-textSecondary">— package version</span></div>
|
|
219
|
+
<div><code class="text-accentLight">{{MODE}}</code> <span class="text-textSecondary">— "autonomous" or "standard"</span></div>
|
|
220
|
+
</div>
|
|
221
|
+
</details>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<!-- Drive Framing Template -->
|
|
225
|
+
<div class="glass rounded-xl p-6 mt-6">
|
|
226
|
+
<div class="flex flex-wrap items-center justify-between gap-4 mb-4">
|
|
227
|
+
<div>
|
|
228
|
+
<h3 class="text-lg font-semibold">Drive Framing Template</h3>
|
|
229
|
+
<p class="text-sm text-textSecondary mt-1">Customize the autonomous drive prompt sent when the operator is away</p>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="flex items-center gap-2">
|
|
232
|
+
<span id="drive-tpl-status" class="text-sm"></span>
|
|
233
|
+
<button onclick="resetDriveTemplate()" class="px-4 py-2 rounded-xl bg-card hover:bg-cardHover border border-gray-700 text-sm text-textSecondary hover:text-textPrimary transition">Reset to Default</button>
|
|
234
|
+
<button onclick="saveDriveTemplate()" class="px-4 py-2 rounded-xl bg-accent hover:bg-accentLight text-white text-sm font-medium transition">Save</button>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="mb-4 flex flex-wrap items-center gap-3">
|
|
238
|
+
<label class="text-sm text-textSecondary">Drive Activation Period:</label>
|
|
239
|
+
<div class="flex items-center gap-2">
|
|
240
|
+
<input id="drive-activation-hours" type="number" min="0.5" step="0.5" class="w-20 px-3 py-1.5 rounded-lg bg-surface border border-gray-700 text-textPrimary font-mono text-sm focus:outline-none focus:border-accent transition" />
|
|
241
|
+
<span class="text-sm text-textSecondary">hours</span>
|
|
242
|
+
</div>
|
|
243
|
+
<span class="text-xs text-muted">(informational — requires server restart via DMN_ACTIVATION_HOURS env var)</span>
|
|
244
|
+
</div>
|
|
245
|
+
<div id="drive-tpl-default-badge" class="hidden mb-3">
|
|
246
|
+
<span class="type-badge" style="background:rgba(245,158,11,0.15);color:#fbbf24">NO CUSTOM TEMPLATE — using hardcoded drive prompts. Load a preset and save to customize.</span>
|
|
247
|
+
</div>
|
|
248
|
+
<div class="flex flex-wrap gap-2 mb-3">
|
|
249
|
+
<span class="text-sm text-textSecondary self-center">Presets:</span>
|
|
250
|
+
<button onclick="loadPreset('operator-instruction')" class="px-3 py-1.5 rounded-lg bg-card hover:bg-cardHover border border-gray-700 text-xs text-textSecondary hover:text-textPrimary transition">📋 Direct Instruction</button>
|
|
251
|
+
<button onclick="loadPreset('operator-notes')" class="px-3 py-1.5 rounded-lg bg-card hover:bg-cardHover border border-gray-700 text-xs text-textSecondary hover:text-textPrimary transition">📝 Operator Notes</button>
|
|
252
|
+
<button onclick="loadPreset('dispatcher')" class="px-3 py-1.5 rounded-lg bg-card hover:bg-cardHover border border-gray-700 text-xs text-textSecondary hover:text-textPrimary transition">⚡ Dispatcher</button>
|
|
253
|
+
<button onclick="loadPreset('first-person')" class="px-3 py-1.5 rounded-lg bg-card hover:bg-cardHover border border-gray-700 text-xs text-textSecondary hover:text-textPrimary transition">💭 First Person</button>
|
|
254
|
+
</div>
|
|
255
|
+
<textarea id="drive-tpl-editor" rows="15" spellcheck="false"
|
|
256
|
+
class="w-full px-4 py-3 rounded-xl bg-surface border border-gray-700 text-textPrimary font-mono text-sm leading-relaxed focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition resize-y"
|
|
257
|
+
placeholder="Load a preset or type a custom drive template..."></textarea>
|
|
258
|
+
<div class="mt-4">
|
|
259
|
+
<div id="drive-tpl-preview-header" class="flex items-center gap-2 mb-2 cursor-pointer select-none" onclick="toggleDriveTplPreview()">
|
|
260
|
+
<svg id="drive-tpl-preview-arrow" class="w-4 h-4 text-textSecondary transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
261
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
262
|
+
</svg>
|
|
263
|
+
<span class="text-sm font-medium text-textSecondary">Preview with sample values</span>
|
|
264
|
+
</div>
|
|
265
|
+
<div id="drive-tpl-preview" class="hidden glass rounded-xl p-4 font-mono text-sm leading-relaxed whitespace-pre-wrap break-words"></div>
|
|
266
|
+
</div>
|
|
267
|
+
<details class="mt-4">
|
|
268
|
+
<summary class="text-sm font-medium text-textSecondary cursor-pointer hover:text-textPrimary transition">Available Variables</summary>
|
|
269
|
+
<div class="mt-2 glass rounded-xl p-4 grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
|
|
270
|
+
<div><code class="text-accentLight">{{IDLE_HOURS}}</code> <span class="text-textSecondary">— hours since last operator interaction</span></div>
|
|
271
|
+
<div><code class="text-accentLight">{{TIME}}</code> <span class="text-textSecondary">— ISO timestamp</span></div>
|
|
272
|
+
<div><code class="text-accentLight">{{PROBABILITY}}</code> <span class="text-textSecondary">— drive activation probability (0.2–1.0)</span></div>
|
|
273
|
+
</div>
|
|
274
|
+
</details>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<script>
|
|
282
|
+
// ─── State ─────────────────────────────────────────────────────────
|
|
283
|
+
let token = localStorage.getItem('sensorium_token') || '';
|
|
284
|
+
let currentTab = 'sessions';
|
|
285
|
+
let refreshTimer = null;
|
|
286
|
+
|
|
287
|
+
// ─── Auth ──────────────────────────────────────────────────────────
|
|
288
|
+
async function authenticate() {
|
|
289
|
+
const input = document.getElementById('token-input');
|
|
290
|
+
token = input.value.trim();
|
|
291
|
+
if (!token) { token = 'no-auth'; } // allow no-auth mode
|
|
292
|
+
try {
|
|
293
|
+
const res = await api('/api/status');
|
|
294
|
+
if (res) {
|
|
295
|
+
localStorage.setItem('sensorium_token', token);
|
|
296
|
+
document.getElementById('auth-overlay').classList.add('hidden');
|
|
297
|
+
document.getElementById('dashboard').classList.remove('hidden');
|
|
298
|
+
startRefresh();
|
|
299
|
+
}
|
|
300
|
+
} catch (e) {
|
|
301
|
+
document.getElementById('auth-error').classList.remove('hidden');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function logout() {
|
|
306
|
+
localStorage.removeItem('sensorium_token');
|
|
307
|
+
token = '';
|
|
308
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
309
|
+
document.getElementById('dashboard').classList.add('hidden');
|
|
310
|
+
document.getElementById('auth-overlay').classList.remove('hidden');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Auto-connect if token saved
|
|
314
|
+
if (token) {
|
|
315
|
+
api('/api/status').then(data => {
|
|
316
|
+
if (data) {
|
|
317
|
+
document.getElementById('auth-overlay').classList.add('hidden');
|
|
318
|
+
document.getElementById('dashboard').classList.remove('hidden');
|
|
319
|
+
startRefresh();
|
|
320
|
+
}
|
|
321
|
+
}).catch(() => {
|
|
322
|
+
localStorage.removeItem('sensorium_token');
|
|
323
|
+
token = '';
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Enter key on token input
|
|
328
|
+
document.getElementById('token-input').addEventListener('keydown', e => {
|
|
329
|
+
if (e.key === 'Enter') authenticate();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ─── API ───────────────────────────────────────────────────────────
|
|
333
|
+
async function api(path) {
|
|
334
|
+
const res = await fetch(path, {
|
|
335
|
+
headers: { 'Authorization': 'Bearer ' + token },
|
|
336
|
+
});
|
|
337
|
+
if (!res.ok) throw new Error(res.statusText);
|
|
338
|
+
return res.json();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ─── Rendering ─────────────────────────────────────────────────────
|
|
342
|
+
function formatUptime(seconds) {
|
|
343
|
+
const d = Math.floor(seconds / 86400);
|
|
344
|
+
const h = Math.floor((seconds % 86400) / 3600);
|
|
345
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
346
|
+
if (d > 0) return d + 'd ' + h + 'h';
|
|
347
|
+
if (h > 0) return h + 'h ' + m + 'm';
|
|
348
|
+
return m + 'm';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function timeAgo(iso) {
|
|
352
|
+
if (!iso) return 'never';
|
|
353
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
354
|
+
const mins = Math.floor(diff / 60000);
|
|
355
|
+
if (mins < 1) return 'just now';
|
|
356
|
+
if (mins < 60) return mins + 'm ago';
|
|
357
|
+
const hours = Math.floor(mins / 60);
|
|
358
|
+
if (hours < 24) return hours + 'h ago';
|
|
359
|
+
return Math.floor(hours / 24) + 'd ago';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function statCard(label, value, icon, color = 'accent') {
|
|
363
|
+
return '<div class="glass rounded-xl p-4 stat-glow animate-slide-up">' +
|
|
364
|
+
'<div class="flex items-center gap-2 mb-2">' +
|
|
365
|
+
'<span class="text-' + color + '">' + icon + '</span>' +
|
|
366
|
+
'<span class="text-xs text-textSecondary font-medium uppercase tracking-wider">' + label + '</span>' +
|
|
367
|
+
'</div>' +
|
|
368
|
+
'<div class="text-2xl font-bold font-mono">' + value + '</div>' +
|
|
369
|
+
'</div>';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function updateEl(el, html) { if (el && el.innerHTML !== html) el.innerHTML = html; }
|
|
373
|
+
|
|
374
|
+
function renderStats(data) {
|
|
375
|
+
const m = data.memory || {};
|
|
376
|
+
const grid = document.getElementById('stats-grid');
|
|
377
|
+
const statsHtml =
|
|
378
|
+
statCard('Sessions', data.activeSessions,
|
|
379
|
+
'<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>',
|
|
380
|
+
'success') +
|
|
381
|
+
statCard('Notes', m.totalSemanticNotes,
|
|
382
|
+
'<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>',
|
|
383
|
+
'accentLight') +
|
|
384
|
+
statCard('Episodes', m.totalEpisodes,
|
|
385
|
+
'<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>',
|
|
386
|
+
'warn') +
|
|
387
|
+
statCard('Unconsolidated', m.unconsolidatedEpisodes,
|
|
388
|
+
'<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>',
|
|
389
|
+
m.unconsolidatedEpisodes > 10 ? 'danger' : 'success') +
|
|
390
|
+
statCard('Procedures', m.totalProcedures,
|
|
391
|
+
'<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>') +
|
|
392
|
+
statCard('Uptime', formatUptime(data.uptime),
|
|
393
|
+
'<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>',
|
|
394
|
+
'success');
|
|
395
|
+
updateEl(grid, statsHtml);
|
|
396
|
+
|
|
397
|
+
document.getElementById('uptime-display').textContent = formatUptime(data.uptime);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function renderSessions(sessions) {
|
|
401
|
+
const list = document.getElementById('sessions-list');
|
|
402
|
+
const empty = document.getElementById('sessions-empty');
|
|
403
|
+
if (!Array.isArray(sessions) || !sessions.length) {
|
|
404
|
+
if (list.innerHTML !== '') list.innerHTML = '';
|
|
405
|
+
empty.classList.remove('hidden');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
empty.classList.add('hidden');
|
|
409
|
+
// Sort: active first, then disconnected by most recent activity
|
|
410
|
+
sessions.sort(function(a, b) {
|
|
411
|
+
if (a.status === 'active' && b.status !== 'active') return -1;
|
|
412
|
+
if (a.status !== 'active' && b.status === 'active') return 1;
|
|
413
|
+
return b.lastActivity - a.lastActivity;
|
|
414
|
+
});
|
|
415
|
+
const html = sessions.map(s => {
|
|
416
|
+
const isDisconnected = s.status === 'disconnected';
|
|
417
|
+
const idle = Math.floor((Date.now() - s.lastActivity) / 60000);
|
|
418
|
+
var statusColor, statusLabel;
|
|
419
|
+
if (isDisconnected) {
|
|
420
|
+
statusColor = 'muted';
|
|
421
|
+
statusLabel = 'Disconnected' + (idle > 0 ? ' — ' + idle + 'm ago' : '');
|
|
422
|
+
} else {
|
|
423
|
+
statusColor = idle < 5 ? 'success' : idle < 30 ? 'warn' : 'danger';
|
|
424
|
+
statusLabel = idle < 5 ? 'Active' : idle < 30 ? 'Idle ' + idle + 'm' : 'Dormant ' + idle + 'm';
|
|
425
|
+
}
|
|
426
|
+
// Wait-for-instructions heartbeat indicator
|
|
427
|
+
var pollLabel = '';
|
|
428
|
+
if (s.lastWaitCallAt) {
|
|
429
|
+
var waitAgo = Math.floor((Date.now() - s.lastWaitCallAt) / 60000);
|
|
430
|
+
if (waitAgo < 5) {
|
|
431
|
+
pollLabel = '<span class="text-xs text-success ml-2">Polling — ' + (waitAgo === 0 ? 'just now' : waitAgo + 'm ago') + '</span>';
|
|
432
|
+
} else {
|
|
433
|
+
pollLabel = '<span class="text-xs text-warn ml-2">Last poll — ' + waitAgo + 'm ago</span>';
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return '<div class="glass rounded-xl p-4 animate-slide-up' + (isDisconnected ? ' opacity-60' : '') + '">' +
|
|
437
|
+
'<div class="flex items-center justify-between">' +
|
|
438
|
+
'<div class="flex items-center gap-3">' +
|
|
439
|
+
'<span class="w-2.5 h-2.5 rounded-full bg-' + statusColor + '"></span>' +
|
|
440
|
+
'<div>' +
|
|
441
|
+
'<div class="font-medium">Thread ' + s.threadId + '</div>' +
|
|
442
|
+
'<div class="text-xs text-textSecondary font-mono">' + s.mcpSessionId.slice(0, 12) + '...</div>' +
|
|
443
|
+
'</div>' +
|
|
444
|
+
'</div>' +
|
|
445
|
+
'<div class="text-right">' +
|
|
446
|
+
'<div class="text-sm font-medium text-' + statusColor + '">' + statusLabel + pollLabel + '</div>' +
|
|
447
|
+
'<div class="text-xs text-textSecondary">' + s.transportType + '</div>' +
|
|
448
|
+
'</div>' +
|
|
449
|
+
'</div>' +
|
|
450
|
+
'</div>';
|
|
451
|
+
}).join('');
|
|
452
|
+
if (list.innerHTML !== html) list.innerHTML = html;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function renderNotes(notes) {
|
|
456
|
+
const list = document.getElementById('notes-list');
|
|
457
|
+
const html = (notes || []).map(n => {
|
|
458
|
+
const pClass = 'priority-' + (n.priority || 0);
|
|
459
|
+
return '<div class="glass rounded-xl p-4 ' + pClass + ' animate-fade-in">' +
|
|
460
|
+
'<div class="flex items-start justify-between gap-3">' +
|
|
461
|
+
'<div class="flex-1 min-w-0">' +
|
|
462
|
+
'<div class="flex items-center gap-2 mb-1">' +
|
|
463
|
+
'<span class="type-badge type-' + n.type + '">' + n.type + '</span>' +
|
|
464
|
+
(n.priority >= 2 ? '<span class="type-badge" style="background:rgba(239,68,68,0.15);color:#f87171">HIGH IMPORTANCE</span>' : '') +
|
|
465
|
+
(n.priority === 1 ? '<span class="type-badge" style="background:rgba(245,158,11,0.15);color:#fbbf24">NOTABLE</span>' : '') +
|
|
466
|
+
'<span class="text-xs text-textSecondary">' + n.noteId + '</span>' +
|
|
467
|
+
'</div>' +
|
|
468
|
+
'<p class="text-sm text-textPrimary leading-relaxed">' + escapeHtml(n.content) + '</p>' +
|
|
469
|
+
'<div class="flex flex-wrap gap-1.5 mt-2">' +
|
|
470
|
+
(n.keywords || []).map(k => '<span class="text-xs px-2 py-0.5 rounded-full bg-accent/10 text-accentLight">' + escapeHtml(k) + '</span>').join('') +
|
|
471
|
+
'</div>' +
|
|
472
|
+
'</div>' +
|
|
473
|
+
'<div class="text-right shrink-0">' +
|
|
474
|
+
'<div class="text-sm font-mono text-textSecondary">' + ((Number(n.confidence) || 0) * 100).toFixed(0) + '%</div>' +
|
|
475
|
+
'<div class="text-xs text-muted">' + timeAgo(n.createdAt) + '</div>' +
|
|
476
|
+
'<div class="text-xs text-muted">' + (n.accessCount ?? 0) + ' hits</div>' +
|
|
477
|
+
'</div>' +
|
|
478
|
+
'</div>' +
|
|
479
|
+
'</div>';
|
|
480
|
+
}).join('');
|
|
481
|
+
if (list.innerHTML !== html) list.innerHTML = html;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function renderEpisodes(episodes) {
|
|
485
|
+
const list = document.getElementById('episodes-list');
|
|
486
|
+
if (!episodes || !episodes.length) { list.innerHTML = '<p class="text-textSecondary text-center py-12">No episodes</p>'; return; }
|
|
487
|
+
const modalityIcons = {
|
|
488
|
+
text: '💬', voice: '🎤', image: '🖼️', file: '📎', system: '⚙️'
|
|
489
|
+
};
|
|
490
|
+
const html = episodes.map(ep => {
|
|
491
|
+
const icon = modalityIcons[ep.modality] || '📝';
|
|
492
|
+
const content = ep.content ? (typeof ep.content === 'object' ? JSON.stringify(ep.content).slice(0, 300) : String(ep.content).slice(0, 300)) : '(no content)';
|
|
493
|
+
const type = ep.type || 'unknown';
|
|
494
|
+
const episodeId = ep.episodeId || '-';
|
|
495
|
+
const importance = (Number(ep.importance) || 0);
|
|
496
|
+
return '<div class="glass rounded-xl p-4 animate-fade-in">' +
|
|
497
|
+
'<div class="flex items-start gap-3">' +
|
|
498
|
+
'<span class="text-lg">' + icon + '</span>' +
|
|
499
|
+
'<div class="flex-1 min-w-0">' +
|
|
500
|
+
'<div class="flex items-center gap-2 mb-1">' +
|
|
501
|
+
'<span class="type-badge type-fact">' + escapeHtml(type) + '</span>' +
|
|
502
|
+
'<span class="text-xs text-textSecondary font-mono">' + escapeHtml(episodeId) + '</span>' +
|
|
503
|
+
'<span class="text-xs text-muted">' + timeAgo(ep.createdAt) + '</span>' +
|
|
504
|
+
'</div>' +
|
|
505
|
+
'<p class="text-sm text-textSecondary leading-relaxed break-words">' + escapeHtml(content) + '</p>' +
|
|
506
|
+
'</div>' +
|
|
507
|
+
'<div class="text-xs text-muted shrink-0">imp: ' + (importance * 100).toFixed(0) + '%</div>' +
|
|
508
|
+
'</div>' +
|
|
509
|
+
'</div>';
|
|
510
|
+
}).join('');
|
|
511
|
+
if (list.innerHTML !== html) list.innerHTML = html;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function renderTopics(topics) {
|
|
515
|
+
const grid = document.getElementById('topics-grid');
|
|
516
|
+
if (!topics.length) { grid.innerHTML = '<p class="text-textSecondary col-span-full text-center py-12">No topics yet</p>'; return; }
|
|
517
|
+
const maxCount = Math.max(...topics.map(t => (t.semanticCount || 0) + (t.proceduralCount || 0))) || 1;
|
|
518
|
+
const html = topics.map(t => {
|
|
519
|
+
const count = (t.semanticCount || 0) + (t.proceduralCount || 0);
|
|
520
|
+
const intensity = Math.max(0.15, count / maxCount);
|
|
521
|
+
return '<div class="glass rounded-xl p-4 animate-slide-up" style="border-left: 3px solid rgba(99,102,241,' + intensity + ')">' +
|
|
522
|
+
'<div class="font-medium text-sm">' + escapeHtml(t.topic || 'Unknown') + '</div>' +
|
|
523
|
+
'<div class="flex items-center justify-between mt-2">' +
|
|
524
|
+
'<span class="text-lg font-bold font-mono text-accent">' + count + '</span>' +
|
|
525
|
+
'<span class="text-xs text-muted">' + timeAgo(t.lastUpdated) + '</span>' +
|
|
526
|
+
'</div>' +
|
|
527
|
+
'</div>';
|
|
528
|
+
}).join('');
|
|
529
|
+
if (grid.innerHTML !== html) grid.innerHTML = html;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function renderRateLimits(data) {
|
|
533
|
+
const summary = document.getElementById('ratelimits-summary');
|
|
534
|
+
const grid = document.getElementById('ratelimits-grid');
|
|
535
|
+
summary.innerHTML =
|
|
536
|
+
'<div class="flex items-center justify-between">' +
|
|
537
|
+
'<div>' +
|
|
538
|
+
'<div class="text-sm text-textSecondary">Active Agents Sharing Resources</div>' +
|
|
539
|
+
'<div class="text-3xl font-bold font-mono text-accent">' + data.activeSessions + '</div>' +
|
|
540
|
+
'</div>' +
|
|
541
|
+
'<div class="text-right">' +
|
|
542
|
+
'<div class="text-sm text-textSecondary">Total Calls (Last Hour)</div>' +
|
|
543
|
+
'<div class="text-3xl font-bold font-mono">' + data.totalCallsLastHour + '</div>' +
|
|
544
|
+
'</div>' +
|
|
545
|
+
'</div>';
|
|
546
|
+
if (!data.services || !data.services.length) {
|
|
547
|
+
grid.innerHTML = '<p class="text-textSecondary col-span-full text-center py-8">No services tracked yet</p>';
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
grid.innerHTML = data.services.map(function(svc) {
|
|
551
|
+
var pct = svc.usagePercent;
|
|
552
|
+
var barColor = pct > 80 ? 'danger' : pct > 50 ? 'warn' : 'success';
|
|
553
|
+
var breakdown = '';
|
|
554
|
+
if (svc.sessionBreakdown && svc.sessionBreakdown.length > 0) {
|
|
555
|
+
breakdown = '<div class="mt-3 space-y-1">' +
|
|
556
|
+
'<div class="text-xs text-textSecondary font-medium uppercase tracking-wider">Per-Session Breakdown</div>' +
|
|
557
|
+
svc.sessionBreakdown.map(function(s) {
|
|
558
|
+
return '<div class="flex items-center justify-between text-xs">' +
|
|
559
|
+
'<span class="text-textSecondary font-mono">Thread ' + (s.threadId || '?') + '</span>' +
|
|
560
|
+
'<span class="font-mono text-textPrimary">' + s.calls + ' calls</span>' +
|
|
561
|
+
'</div>';
|
|
562
|
+
}).join('') +
|
|
563
|
+
'</div>';
|
|
564
|
+
}
|
|
565
|
+
return '<div class="glass rounded-xl p-4 animate-slide-up">' +
|
|
566
|
+
'<div class="flex items-center justify-between mb-3">' +
|
|
567
|
+
'<div>' +
|
|
568
|
+
'<div class="font-medium text-sm">' + escapeHtml(svc.description) + '</div>' +
|
|
569
|
+
'<div class="text-xs text-muted font-mono">' + escapeHtml(svc.service) + '</div>' +
|
|
570
|
+
'</div>' +
|
|
571
|
+
'<div class="text-right">' +
|
|
572
|
+
'<div class="text-lg font-bold font-mono text-' + barColor + '">' + pct + '%</div>' +
|
|
573
|
+
'</div>' +
|
|
574
|
+
'</div>' +
|
|
575
|
+
'<div class="w-full h-2 bg-surface rounded-full overflow-hidden mb-3">' +
|
|
576
|
+
'<div class="h-full bg-' + barColor + ' rounded-full transition-all" style="width:' + Math.min(pct, 100) + '%"></div>' +
|
|
577
|
+
'</div>' +
|
|
578
|
+
'<div class="grid grid-cols-2 gap-2 text-xs">' +
|
|
579
|
+
'<div><span class="text-textSecondary">Window:</span> <span class="font-mono">' + svc.callsInWindow + '/' + svc.maxPerWindow + '</span></div>' +
|
|
580
|
+
'<div><span class="text-textSecondary">Burst:</span> <span class="font-mono">' + svc.availableTokens + '/' + svc.burstCapacity + '</span></div>' +
|
|
581
|
+
'</div>' +
|
|
582
|
+
breakdown +
|
|
583
|
+
'</div>';
|
|
584
|
+
}).join('');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ─── Tab switching ──────────────────────────────────────────────────
|
|
588
|
+
function switchTab(tab) {
|
|
589
|
+
const tabs = ['sessions', 'notes', 'episodes', 'topics', 'templates'];
|
|
590
|
+
tabs.forEach(t => {
|
|
591
|
+
document.getElementById('panel-' + t).classList.toggle('hidden', t !== tab);
|
|
592
|
+
document.getElementById('tab-' + t).className = 'pb-3 text-sm font-medium transition ' + (t === tab ? 'tab-active' : 'tab-inactive');
|
|
593
|
+
});
|
|
594
|
+
currentTab = tab;
|
|
595
|
+
refreshCurrentTab();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ─── Data loading ─────────────────────────────────────────────────
|
|
599
|
+
let searchDebounce = null;
|
|
600
|
+
document.getElementById('notes-search')?.addEventListener('input', () => {
|
|
601
|
+
clearTimeout(searchDebounce);
|
|
602
|
+
searchDebounce = setTimeout(() => loadNotes(), 300);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
async function loadNotes() {
|
|
606
|
+
try {
|
|
607
|
+
const q = document.getElementById('notes-search').value.trim();
|
|
608
|
+
const type = document.getElementById('notes-type').value;
|
|
609
|
+
const sort = document.getElementById('notes-sort').value;
|
|
610
|
+
let notes;
|
|
611
|
+
if (q) {
|
|
612
|
+
notes = await api('/api/search?q=' + encodeURIComponent(q) + '&limit=50');
|
|
613
|
+
} else {
|
|
614
|
+
notes = await api('/api/notes?limit=50' + (type ? '&type=' + type : '') + '&sort=' + sort);
|
|
615
|
+
}
|
|
616
|
+
renderNotes(notes);
|
|
617
|
+
} catch (e) { console.error('Notes load error:', e); }
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function loadEpisodes() {
|
|
621
|
+
try {
|
|
622
|
+
const threadId = document.getElementById('episodes-thread').value;
|
|
623
|
+
let url = '/api/episodes?limit=30';
|
|
624
|
+
if (threadId) url += '&threadId=' + threadId;
|
|
625
|
+
const episodes = await api(url);
|
|
626
|
+
renderEpisodes(episodes);
|
|
627
|
+
} catch (e) { console.error('Episodes load error:', e); }
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function loadTopics() {
|
|
631
|
+
try {
|
|
632
|
+
const topics = await api('/api/topics');
|
|
633
|
+
renderTopics(topics);
|
|
634
|
+
} catch (e) { console.error('Topics load error:', e); }
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function refreshCurrentTab() {
|
|
638
|
+
try {
|
|
639
|
+
const data = await api('/api/status').catch(() => null);
|
|
640
|
+
if (data && data.memory) {
|
|
641
|
+
renderStats(data);
|
|
642
|
+
renderSessions(data.sessions || []);
|
|
643
|
+
} else if (data && data.error) {
|
|
644
|
+
console.error('Dashboard API error:', data.error);
|
|
645
|
+
}
|
|
646
|
+
} catch (e) {
|
|
647
|
+
console.error('Dashboard refresh error:', e);
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
if (currentTab === 'notes') loadNotes();
|
|
651
|
+
if (currentTab === 'episodes') loadEpisodes();
|
|
652
|
+
if (currentTab === 'topics') loadTopics();
|
|
653
|
+
if (currentTab === 'templates') { loadTemplates(); loadDriveTemplate(); }
|
|
654
|
+
} catch (e) {
|
|
655
|
+
console.error('Tab load error:', e);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function startRefresh() {
|
|
660
|
+
refreshCurrentTab();
|
|
661
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
662
|
+
refreshTimer = setInterval(refreshCurrentTab, 30000);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ─── Templates ────────────────────────────────────────────────────
|
|
666
|
+
let tplPreviewOpen = false;
|
|
667
|
+
|
|
668
|
+
async function loadTemplates() {
|
|
669
|
+
try {
|
|
670
|
+
const data = await api('/api/templates');
|
|
671
|
+
if (data.templates && data.templates.length > 0) {
|
|
672
|
+
const tpl = data.templates[0];
|
|
673
|
+
document.getElementById('tpl-editor').value = tpl.content;
|
|
674
|
+
document.getElementById('tpl-default-badge').classList.toggle('hidden', !tpl.isDefault);
|
|
675
|
+
updateTplPreview();
|
|
676
|
+
}
|
|
677
|
+
} catch (e) { console.error('Templates load error:', e); }
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function saveTemplate() {
|
|
681
|
+
const status = document.getElementById('tpl-status');
|
|
682
|
+
try {
|
|
683
|
+
const content = document.getElementById('tpl-editor').value;
|
|
684
|
+
const r = await fetch('/api/templates/reminders', {
|
|
685
|
+
method: 'POST',
|
|
686
|
+
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
|
687
|
+
body: JSON.stringify({ content }),
|
|
688
|
+
});
|
|
689
|
+
if (!r.ok) throw new Error(r.statusText);
|
|
690
|
+
await r.json();
|
|
691
|
+
status.textContent = 'Saved ✓';
|
|
692
|
+
status.className = 'text-sm text-success';
|
|
693
|
+
document.getElementById('tpl-default-badge').classList.add('hidden');
|
|
694
|
+
setTimeout(function() { status.textContent = ''; }, 3000);
|
|
695
|
+
} catch (e) {
|
|
696
|
+
status.textContent = 'Error: ' + e.message;
|
|
697
|
+
status.className = 'text-sm text-danger';
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function resetTemplate() {
|
|
702
|
+
if (!confirm('Reset to default template? Your customizations will be lost.')) return;
|
|
703
|
+
const status = document.getElementById('tpl-status');
|
|
704
|
+
try {
|
|
705
|
+
const r = await fetch('/api/templates/reminders', {
|
|
706
|
+
method: 'DELETE',
|
|
707
|
+
headers: { 'Authorization': 'Bearer ' + token },
|
|
708
|
+
});
|
|
709
|
+
if (!r.ok) throw new Error(r.statusText);
|
|
710
|
+
await r.json();
|
|
711
|
+
await loadTemplates();
|
|
712
|
+
status.textContent = 'Reset to default ✓';
|
|
713
|
+
status.className = 'text-sm text-success';
|
|
714
|
+
setTimeout(function() { status.textContent = ''; }, 3000);
|
|
715
|
+
} catch (e) {
|
|
716
|
+
status.textContent = 'Error: ' + e.message;
|
|
717
|
+
status.className = 'text-sm text-danger';
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function updateTplPreview() {
|
|
722
|
+
const content = document.getElementById('tpl-editor').value;
|
|
723
|
+
const preview = document.getElementById('tpl-preview');
|
|
724
|
+
const highlighted = escapeHtml(content).replace(/{{([A-Z_]+)}}/g,
|
|
725
|
+
'<span class="text-accentLight bg-accent/10 px-1 rounded">{{$1}}</span>');
|
|
726
|
+
preview.innerHTML = highlighted;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function toggleTplPreview() {
|
|
730
|
+
tplPreviewOpen = !tplPreviewOpen;
|
|
731
|
+
document.getElementById('tpl-preview').classList.toggle('hidden', !tplPreviewOpen);
|
|
732
|
+
document.getElementById('tpl-preview-arrow').style.transform = tplPreviewOpen ? 'rotate(90deg)' : '';
|
|
733
|
+
if (tplPreviewOpen) updateTplPreview();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
document.getElementById('tpl-editor')?.addEventListener('input', function() {
|
|
737
|
+
if (tplPreviewOpen) updateTplPreview();
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// ─── Drive Templates ──────────────────────────────────────────────
|
|
741
|
+
let drivePresets = [];
|
|
742
|
+
let driveTplPreviewOpen = false;
|
|
743
|
+
|
|
744
|
+
async function loadDriveTemplate() {
|
|
745
|
+
try {
|
|
746
|
+
const [driveData, presetsData, settingsData] = await Promise.all([
|
|
747
|
+
api('/api/templates/drive'),
|
|
748
|
+
api('/api/templates/drive-presets'),
|
|
749
|
+
api('/api/settings/dmn-activation-hours'),
|
|
750
|
+
]);
|
|
751
|
+
drivePresets = presetsData.presets || [];
|
|
752
|
+
document.getElementById('drive-activation-hours').value = settingsData.value;
|
|
753
|
+
if (driveData.content) {
|
|
754
|
+
document.getElementById('drive-tpl-editor').value = driveData.content;
|
|
755
|
+
document.getElementById('drive-tpl-default-badge').classList.add('hidden');
|
|
756
|
+
} else {
|
|
757
|
+
document.getElementById('drive-tpl-editor').value = '';
|
|
758
|
+
document.getElementById('drive-tpl-default-badge').classList.remove('hidden');
|
|
759
|
+
}
|
|
760
|
+
updateDriveTplPreview();
|
|
761
|
+
} catch (e) { console.error('Drive template load error:', e); }
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async function saveDriveTemplate() {
|
|
765
|
+
const status = document.getElementById('drive-tpl-status');
|
|
766
|
+
try {
|
|
767
|
+
const content = document.getElementById('drive-tpl-editor').value;
|
|
768
|
+
if (!content.trim()) {
|
|
769
|
+
status.textContent = 'Template is empty — load a preset first';
|
|
770
|
+
status.className = 'text-sm text-warn';
|
|
771
|
+
setTimeout(function() { status.textContent = ''; }, 3000);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const r = await fetch('/api/templates/drive', {
|
|
775
|
+
method: 'POST',
|
|
776
|
+
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
|
777
|
+
body: JSON.stringify({ content }),
|
|
778
|
+
});
|
|
779
|
+
if (!r.ok) throw new Error(r.statusText);
|
|
780
|
+
await r.json();
|
|
781
|
+
status.textContent = 'Saved ✓';
|
|
782
|
+
status.className = 'text-sm text-success';
|
|
783
|
+
document.getElementById('drive-tpl-default-badge').classList.add('hidden');
|
|
784
|
+
setTimeout(function() { status.textContent = ''; }, 3000);
|
|
785
|
+
} catch (e) {
|
|
786
|
+
status.textContent = 'Error: ' + e.message;
|
|
787
|
+
status.className = 'text-sm text-danger';
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function resetDriveTemplate() {
|
|
792
|
+
if (!confirm('Reset drive template? The custom template will be removed and the system will use hardcoded drive prompts.')) return;
|
|
793
|
+
const status = document.getElementById('drive-tpl-status');
|
|
794
|
+
try {
|
|
795
|
+
const r = await fetch('/api/templates/drive', {
|
|
796
|
+
method: 'DELETE',
|
|
797
|
+
headers: { 'Authorization': 'Bearer ' + token },
|
|
798
|
+
});
|
|
799
|
+
if (!r.ok) throw new Error(r.statusText);
|
|
800
|
+
await r.json();
|
|
801
|
+
document.getElementById('drive-tpl-editor').value = '';
|
|
802
|
+
document.getElementById('drive-tpl-default-badge').classList.remove('hidden');
|
|
803
|
+
status.textContent = 'Reset to default ✓';
|
|
804
|
+
status.className = 'text-sm text-success';
|
|
805
|
+
setTimeout(function() { status.textContent = ''; }, 3000);
|
|
806
|
+
} catch (e) {
|
|
807
|
+
status.textContent = 'Error: ' + e.message;
|
|
808
|
+
status.className = 'text-sm text-danger';
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function loadPreset(key) {
|
|
813
|
+
var preset = drivePresets.find(function(p) { return p.key === key; });
|
|
814
|
+
if (preset) {
|
|
815
|
+
document.getElementById('drive-tpl-editor').value = preset.content;
|
|
816
|
+
document.getElementById('drive-tpl-default-badge').classList.add('hidden');
|
|
817
|
+
updateDriveTplPreview();
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function updateDriveTplPreview() {
|
|
822
|
+
var content = document.getElementById('drive-tpl-editor').value;
|
|
823
|
+
var preview = document.getElementById('drive-tpl-preview');
|
|
824
|
+
if (!content) {
|
|
825
|
+
preview.innerHTML = '<span class="text-muted">(no template loaded)</span>';
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
var highlighted = escapeHtml(content).replace(/{{([A-Z_]+)}}/g,
|
|
829
|
+
'<span class="text-accentLight bg-accent/10 px-1 rounded">{{$1}}</span>');
|
|
830
|
+
preview.innerHTML = highlighted;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function toggleDriveTplPreview() {
|
|
834
|
+
driveTplPreviewOpen = !driveTplPreviewOpen;
|
|
835
|
+
document.getElementById('drive-tpl-preview').classList.toggle('hidden', !driveTplPreviewOpen);
|
|
836
|
+
document.getElementById('drive-tpl-preview-arrow').style.transform = driveTplPreviewOpen ? 'rotate(90deg)' : '';
|
|
837
|
+
if (driveTplPreviewOpen) updateDriveTplPreview();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
document.getElementById('drive-tpl-editor')?.addEventListener('input', function() {
|
|
841
|
+
if (driveTplPreviewOpen) updateDriveTplPreview();
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
// ─── Utilities ──────────────────────────────────────────────────────
|
|
845
|
+
function escapeHtml(str) {
|
|
846
|
+
if (!str) return '';
|
|
847
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
848
|
+
}
|
|
849
|
+
</script>
|
|
850
|
+
</body>
|
|
851
|
+
</html>
|
|
@@ -186,7 +186,7 @@ async function handleHibernate(args, ctx, extra) {
|
|
|
186
186
|
type: "text",
|
|
187
187
|
text: `⚠️ **Server update pending** (${maintenanceInfo}). ` +
|
|
188
188
|
`The MCP server will restart shortly. Use Desktop Commander to run: ` +
|
|
189
|
-
`Start-Sleep -Seconds
|
|
189
|
+
`Start-Sleep -Seconds 600 — then call start_session with threadId=${effectiveThreadId} to reconnect.` +
|
|
190
190
|
getShortReminder(effectiveThreadId),
|
|
191
191
|
}],
|
|
192
192
|
};
|
|
@@ -69,7 +69,7 @@ export async function handleWaitForInstructions(args, ctx, extra) {
|
|
|
69
69
|
type: "text",
|
|
70
70
|
text: `⚠️ **Server update pending** (${maintenanceInfo}). ` +
|
|
71
71
|
`The MCP server will restart shortly. Use Desktop Commander to run: ` +
|
|
72
|
-
`Start-Sleep -Seconds
|
|
72
|
+
`Start-Sleep -Seconds 600 — then call start_session with threadId=${effectiveThreadId} to reconnect.` +
|
|
73
73
|
getShortReminder(effectiveThreadId, state.sessionStartedAt),
|
|
74
74
|
}],
|
|
75
75
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sensorium-mcp",
|
|
3
|
-
"version": "2.16.
|
|
3
|
+
"version": "2.16.36",
|
|
4
4
|
"description": "MCP server for remote control of AI assistants via Telegram",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"templates"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
|
-
"build": "tsc",
|
|
14
|
+
"build": "tsc && node -e \"const{mkdirSync,copyFileSync}=require('fs'),{join}=require('path');mkdirSync(join('dist','dashboard'),{recursive:true});copyFileSync(join('src','dashboard','spa.html'),join('dist','dashboard','spa.html'))\"",
|
|
15
15
|
"start": "node dist/index.js",
|
|
16
16
|
"dev": "tsx src/index.ts",
|
|
17
17
|
"prepublishOnly": "npm run build",
|