prism-mcp-server 7.3.1 → 7.4.0
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 +117 -194
- package/dist/cli.js +50 -0
- package/dist/darkfactory/clawInvocation.js +62 -7
- package/dist/darkfactory/runner.js +288 -24
- package/dist/darkfactory/safetyController.js +48 -22
- package/dist/darkfactory/schema.js +2 -0
- package/dist/dashboard/ui.js +2617 -2051
- package/dist/dashboard/ui.tmp.js +3475 -0
- package/dist/errors.js +29 -0
- package/dist/server.js +19 -0
- package/dist/storage/sqlite.js +199 -7
- package/dist/storage/supabase.js +143 -3
- package/dist/tools/routerExperience.js +14 -0
- package/dist/verification/clawValidator.js +2 -1
- package/dist/verification/cliHandler.js +325 -0
- package/dist/verification/gatekeeper.js +39 -0
- package/dist/verification/renameDetector.js +170 -0
- package/dist/verification/runner.js +27 -5
- package/dist/verification/schema.js +18 -0
- package/dist/verification/severityPolicy.js +5 -1
- package/package.json +5 -2
|
@@ -0,0 +1,3475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mind Palace Dashboard — UI Renderer (v2.3.7)
|
|
3
|
+
*
|
|
4
|
+
* Pure CSS + Vanilla JS single-page dashboard.
|
|
5
|
+
* No build step, no Tailwind, no framework — served as a template literal.
|
|
6
|
+
*
|
|
7
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
8
|
+
* DESIGN:
|
|
9
|
+
* - Dark glassmorphism theme with purple/blue gradients
|
|
10
|
+
* - Animated neural network background
|
|
11
|
+
* - Auto-discovers projects on load
|
|
12
|
+
* - Real-time data from storage API
|
|
13
|
+
* - Responsive grid layout
|
|
14
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
15
|
+
*/
|
|
16
|
+
export function renderDashboardHTML(version) {
|
|
17
|
+
return `<!DOCTYPE html>
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charset="UTF-8">
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
22
|
+
<title>Prism MCP — Mind Palace</title>
|
|
23
|
+
<!-- PWA Metadata -->
|
|
24
|
+
<link rel="manifest" href="/manifest.json">
|
|
25
|
+
<meta name="theme-color" content="#0a0e1a">
|
|
26
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
27
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
28
|
+
<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">
|
|
29
|
+
<!-- Vis.js for Neural Graph (v2.3.0) -->
|
|
30
|
+
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
|
31
|
+
<style>
|
|
32
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
33
|
+
|
|
34
|
+
/* ─── Theme: Dark (Default) ─── */
|
|
35
|
+
:root, [data-theme="dark"] {
|
|
36
|
+
--bg-primary: #0a0e1a;
|
|
37
|
+
--bg-secondary: #111827;
|
|
38
|
+
--bg-glass: rgba(17, 24, 39, 0.6);
|
|
39
|
+
--border-glass: rgba(139, 92, 246, 0.15);
|
|
40
|
+
--border-glow: rgba(139, 92, 246, 0.3);
|
|
41
|
+
--text-primary: #f1f5f9;
|
|
42
|
+
--text-secondary: #94a3b8;
|
|
43
|
+
--text-muted: #64748b;
|
|
44
|
+
--accent-purple: #8b5cf6;
|
|
45
|
+
--accent-blue: #3b82f6;
|
|
46
|
+
--accent-cyan: #06b6d4;
|
|
47
|
+
--accent-green: #10b981;
|
|
48
|
+
--accent-amber: #f59e0b;
|
|
49
|
+
--accent-rose: #f43f5e;
|
|
50
|
+
--gradient-hero: linear-gradient(135deg, #8b5cf6 0%, #3b82f6 50%, #06b6d4 100%);
|
|
51
|
+
--radius: 16px;
|
|
52
|
+
--radius-sm: 10px;
|
|
53
|
+
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
|
|
54
|
+
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ─── Theme: Midnight — deeper blacks, blue-shifted accents ─── */
|
|
58
|
+
[data-theme="midnight"] {
|
|
59
|
+
--bg-primary: #020617;
|
|
60
|
+
--bg-secondary: #0f172a;
|
|
61
|
+
--bg-glass: rgba(2, 6, 23, 0.7);
|
|
62
|
+
--border-glass: rgba(59, 130, 246, 0.15);
|
|
63
|
+
--border-glow: rgba(59, 130, 246, 0.35);
|
|
64
|
+
--text-primary: #e2e8f0;
|
|
65
|
+
--text-secondary: #94a3b8;
|
|
66
|
+
--text-muted: #475569;
|
|
67
|
+
--accent-purple: #818cf8;
|
|
68
|
+
--accent-blue: #60a5fa;
|
|
69
|
+
--accent-cyan: #22d3ee;
|
|
70
|
+
--accent-green: #34d399;
|
|
71
|
+
--accent-amber: #fbbf24;
|
|
72
|
+
--accent-rose: #fb7185;
|
|
73
|
+
--gradient-hero: linear-gradient(135deg, #818cf8 0%, #60a5fa 50%, #22d3ee 100%);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ─── Theme: Purple Haze — warm violet tones ─── */
|
|
77
|
+
[data-theme="purple"] {
|
|
78
|
+
--bg-primary: #0c0515;
|
|
79
|
+
--bg-secondary: #1a0a2e;
|
|
80
|
+
--bg-glass: rgba(26, 10, 46, 0.65);
|
|
81
|
+
--border-glass: rgba(168, 85, 247, 0.2);
|
|
82
|
+
--border-glow: rgba(168, 85, 247, 0.4);
|
|
83
|
+
--text-primary: #f5f3ff;
|
|
84
|
+
--text-secondary: #c4b5fd;
|
|
85
|
+
--text-muted: #7c3aed;
|
|
86
|
+
--accent-purple: #a855f7;
|
|
87
|
+
--accent-blue: #7c3aed;
|
|
88
|
+
--accent-cyan: #c084fc;
|
|
89
|
+
--accent-green: #a78bfa;
|
|
90
|
+
--accent-amber: #e879f9;
|
|
91
|
+
--accent-rose: #f472b6;
|
|
92
|
+
--gradient-hero: linear-gradient(135deg, #a855f7 0%, #7c3aed 50%, #c084fc 100%);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
body {
|
|
96
|
+
background: var(--bg-primary);
|
|
97
|
+
color: var(--text-primary);
|
|
98
|
+
font-family: var(--font-sans);
|
|
99
|
+
min-height: 100vh;
|
|
100
|
+
overflow-x: hidden;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ─── Animated Background ─── */
|
|
104
|
+
.bg-grid {
|
|
105
|
+
position: fixed; inset: 0; z-index: 0;
|
|
106
|
+
background-image:
|
|
107
|
+
radial-gradient(circle at 20% 30%, rgba(139,92,246,0.08) 0%, transparent 50%),
|
|
108
|
+
radial-gradient(circle at 80% 70%, rgba(59,130,246,0.06) 0%, transparent 50%),
|
|
109
|
+
radial-gradient(circle at 50% 50%, rgba(6,182,212,0.04) 0%, transparent 60%);
|
|
110
|
+
animation: bgPulse 8s ease-in-out infinite alternate;
|
|
111
|
+
}
|
|
112
|
+
@keyframes bgPulse {
|
|
113
|
+
0% { opacity: 0.6; }
|
|
114
|
+
100% { opacity: 1; }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.container { position: relative; z-index: 1; max-width: 1280px; margin: 0 auto; padding: 2rem; }
|
|
118
|
+
|
|
119
|
+
/* ─── Header ─── */
|
|
120
|
+
header {
|
|
121
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
122
|
+
padding-bottom: 1.5rem; margin-bottom: 2rem;
|
|
123
|
+
border-bottom: 1px solid var(--border-glass);
|
|
124
|
+
}
|
|
125
|
+
.logo {
|
|
126
|
+
font-size: 1.75rem; font-weight: 700;
|
|
127
|
+
background: var(--gradient-hero); -webkit-background-clip: text;
|
|
128
|
+
background-clip: text; -webkit-text-fill-color: transparent;
|
|
129
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
130
|
+
}
|
|
131
|
+
.logo-icon { -webkit-text-fill-color: initial; font-size: 1.5rem; }
|
|
132
|
+
.version-badge {
|
|
133
|
+
font-size: 0.7rem; font-weight: 500; padding: 0.2rem 0.6rem;
|
|
134
|
+
border-radius: 999px; background: rgba(139,92,246,0.15);
|
|
135
|
+
color: var(--accent-purple); border: 1px solid rgba(139,92,246,0.3);
|
|
136
|
+
-webkit-text-fill-color: initial;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* ─── Project Selector ─── */
|
|
140
|
+
.selector {
|
|
141
|
+
display: flex; gap: 0.75rem; align-items: center;
|
|
142
|
+
}
|
|
143
|
+
.selector select, .selector button {
|
|
144
|
+
font-family: var(--font-sans); font-size: 0.875rem;
|
|
145
|
+
border-radius: var(--radius-sm); outline: none;
|
|
146
|
+
transition: all 0.2s ease;
|
|
147
|
+
}
|
|
148
|
+
.selector select {
|
|
149
|
+
background: var(--bg-secondary); color: var(--text-primary);
|
|
150
|
+
border: 1px solid var(--border-glass); padding: 0.6rem 1rem;
|
|
151
|
+
min-width: 220px; cursor: pointer;
|
|
152
|
+
}
|
|
153
|
+
.selector select:hover { border-color: var(--border-glow); }
|
|
154
|
+
.selector button {
|
|
155
|
+
background: var(--gradient-hero); color: white; border: none;
|
|
156
|
+
padding: 0.6rem 1.25rem; font-weight: 600; cursor: pointer;
|
|
157
|
+
}
|
|
158
|
+
.selector button:hover { opacity: 0.9; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(139,92,246,0.3); }
|
|
159
|
+
.selector button:active { transform: translateY(0); }
|
|
160
|
+
|
|
161
|
+
/* ─── Glass Cards ─── */
|
|
162
|
+
.card {
|
|
163
|
+
background: var(--bg-glass); backdrop-filter: blur(16px);
|
|
164
|
+
border: 1px solid var(--border-glass); border-radius: var(--radius);
|
|
165
|
+
padding: 1.5rem; transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
|
166
|
+
}
|
|
167
|
+
.card:hover { border-color: var(--border-glow); box-shadow: 0 0 20px rgba(139,92,246,0.05); }
|
|
168
|
+
.card-title {
|
|
169
|
+
font-size: 0.8rem; font-weight: 600; text-transform: uppercase;
|
|
170
|
+
letter-spacing: 0.1em; margin-bottom: 1rem; display: flex;
|
|
171
|
+
align-items: center; gap: 0.5rem;
|
|
172
|
+
}
|
|
173
|
+
.card-title .dot {
|
|
174
|
+
width: 8px; height: 8px; border-radius: 50%; display: inline-block;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ─── Grid Layout ─── */
|
|
178
|
+
.grid { display: grid; gap: 1.5rem; }
|
|
179
|
+
.grid-main { grid-template-columns: 1fr 2fr; }
|
|
180
|
+
@media (max-width: 900px) { .grid-main { grid-template-columns: 1fr; } }
|
|
181
|
+
|
|
182
|
+
/* ─── PWA Mobile Overrides (v5.4) ─── */
|
|
183
|
+
@media (max-width: 600px) {
|
|
184
|
+
.container { padding: 1rem; }
|
|
185
|
+
header { flex-direction: column; align-items: flex-start; gap: 1rem; }
|
|
186
|
+
.selector { width: 100%; flex-wrap: wrap; }
|
|
187
|
+
.selector select { flex: 1; min-width: 0; }
|
|
188
|
+
|
|
189
|
+
/* Swipeable Columns via CSS Scroll Snap */
|
|
190
|
+
.grid-main {
|
|
191
|
+
display: flex;
|
|
192
|
+
overflow-x: auto;
|
|
193
|
+
scroll-snap-type: x mandatory;
|
|
194
|
+
-webkit-overflow-scrolling: touch;
|
|
195
|
+
gap: 0;
|
|
196
|
+
margin: 0 -1rem; /* bleed to edge */
|
|
197
|
+
padding-bottom: 1rem;
|
|
198
|
+
scrollbar-width: none; /* Hide scrollbar Firefox */
|
|
199
|
+
}
|
|
200
|
+
.grid-main::-webkit-scrollbar { display: none; } /* Hide scrollbar Chrome/Safari */
|
|
201
|
+
.grid-main > .grid {
|
|
202
|
+
flex: 0 0 100%;
|
|
203
|
+
scroll-snap-align: start;
|
|
204
|
+
padding: 0 1rem;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ─── State Panel ─── */
|
|
209
|
+
.summary-text { color: var(--text-secondary); font-size: 0.9rem; line-height: 1.7; margin-bottom: 1rem; }
|
|
210
|
+
.todo-list { list-style: none; padding: 0; }
|
|
211
|
+
.todo-list li {
|
|
212
|
+
padding: 0.5rem 0; border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
213
|
+
font-size: 0.85rem; color: var(--text-secondary);
|
|
214
|
+
display: flex; align-items: flex-start; gap: 0.5rem;
|
|
215
|
+
}
|
|
216
|
+
.todo-list li::before { content: '→'; color: var(--accent-cyan); font-weight: 600; flex-shrink: 0; }
|
|
217
|
+
.todo-list li:last-child { border-bottom: none; }
|
|
218
|
+
|
|
219
|
+
/* ─── Git Metadata ─── */
|
|
220
|
+
.git-row {
|
|
221
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
222
|
+
padding: 0.5rem 0; border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
223
|
+
font-size: 0.85rem;
|
|
224
|
+
}
|
|
225
|
+
.git-row:last-child { border-bottom: none; }
|
|
226
|
+
.git-label { color: var(--text-muted); }
|
|
227
|
+
.git-value { font-family: var(--font-mono); color: var(--text-primary); font-size: 0.8rem; }
|
|
228
|
+
|
|
229
|
+
/* ─── Timeline Items ─── */
|
|
230
|
+
.timeline { display: flex; flex-direction: column; gap: 0.75rem; max-height: 400px; overflow-y: auto; }
|
|
231
|
+
.timeline::-webkit-scrollbar { width: 4px; }
|
|
232
|
+
.timeline::-webkit-scrollbar-track { background: transparent; }
|
|
233
|
+
.timeline::-webkit-scrollbar-thumb { background: var(--border-glass); border-radius: 2px; }
|
|
234
|
+
|
|
235
|
+
.timeline-item {
|
|
236
|
+
padding: 0.875rem 1rem; background: rgba(15,23,42,0.6);
|
|
237
|
+
border-radius: var(--radius-sm); border-left: 3px solid var(--accent-amber);
|
|
238
|
+
font-size: 0.85rem; color: var(--text-secondary); line-height: 1.5;
|
|
239
|
+
transition: background 0.2s ease;
|
|
240
|
+
}
|
|
241
|
+
.timeline-item:hover { background: rgba(15,23,42,0.9); }
|
|
242
|
+
.timeline-item.history { border-left-color: var(--accent-purple); }
|
|
243
|
+
.timeline-item .meta {
|
|
244
|
+
font-size: 0.7rem; font-family: var(--font-mono);
|
|
245
|
+
color: var(--text-muted); margin-bottom: 0.25rem;
|
|
246
|
+
display: flex; justify-content: space-between;
|
|
247
|
+
}
|
|
248
|
+
.timeline-item .badge {
|
|
249
|
+
display: inline-block; padding: 0.1rem 0.4rem; border-radius: 4px;
|
|
250
|
+
font-size: 0.65rem; font-weight: 600; text-transform: uppercase;
|
|
251
|
+
}
|
|
252
|
+
.badge-purple { background: rgba(139,92,246,0.2); color: var(--accent-purple); }
|
|
253
|
+
.badge-amber { background: rgba(245,158,11,0.2); color: var(--accent-amber); }
|
|
254
|
+
.badge-green { background: rgba(16,185,129,0.2); color: var(--accent-green); }
|
|
255
|
+
|
|
256
|
+
.briefing-text {
|
|
257
|
+
font-size: 0.9rem; color: var(--text-secondary); line-height: 1.8;
|
|
258
|
+
white-space: pre-wrap;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* ─── Visual Memory ─── */
|
|
262
|
+
.visual-list { list-style: none; padding: 0; }
|
|
263
|
+
.visual-list li {
|
|
264
|
+
padding: 0.5rem 0; border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
265
|
+
font-size: 0.85rem; color: var(--text-secondary);
|
|
266
|
+
display: flex; align-items: flex-start; gap: 0.5rem;
|
|
267
|
+
}
|
|
268
|
+
.visual-list li:last-child { border-bottom: none; }
|
|
269
|
+
.visual-id {
|
|
270
|
+
font-family: var(--font-mono); font-size: 0.75rem;
|
|
271
|
+
color: var(--accent-rose); font-weight: 500;
|
|
272
|
+
}
|
|
273
|
+
.visual-date {
|
|
274
|
+
font-size: 0.7rem; color: var(--text-muted); margin-left: auto;
|
|
275
|
+
font-family: var(--font-mono); white-space: nowrap;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* ─── Empty / Loading States ─── */
|
|
279
|
+
.empty {
|
|
280
|
+
text-align: center; padding: 3rem 1rem; color: var(--text-muted);
|
|
281
|
+
font-size: 0.9rem;
|
|
282
|
+
}
|
|
283
|
+
.empty .emoji { font-size: 2.5rem; margin-bottom: 0.75rem; }
|
|
284
|
+
.loading { display: none; text-align: center; padding: 2rem; color: var(--accent-purple); }
|
|
285
|
+
.spinner {
|
|
286
|
+
display: inline-block; width: 24px; height: 24px;
|
|
287
|
+
border: 3px solid rgba(139,92,246,0.2); border-top-color: var(--accent-purple);
|
|
288
|
+
border-radius: 50%; animation: spin 0.8s linear infinite;
|
|
289
|
+
margin-right: 0.5rem; vertical-align: middle;
|
|
290
|
+
}
|
|
291
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
292
|
+
|
|
293
|
+
#content { display: none; }
|
|
294
|
+
|
|
295
|
+
/* ─── Fade in animation ─── */
|
|
296
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
297
|
+
.fade-in { animation: fadeIn 0.4s ease-out forwards; }
|
|
298
|
+
|
|
299
|
+
/* ─── Brain Health Indicator (v2.2.0) ─── */
|
|
300
|
+
.health-status {
|
|
301
|
+
display: flex; align-items: center; gap: 0.75rem;
|
|
302
|
+
padding: 0.75rem 1rem; border-radius: var(--radius-sm);
|
|
303
|
+
background: rgba(15,23,42,0.6); margin-bottom: 1rem;
|
|
304
|
+
}
|
|
305
|
+
.health-dot {
|
|
306
|
+
width: 12px; height: 12px; border-radius: 50%;
|
|
307
|
+
flex-shrink: 0; position: relative;
|
|
308
|
+
}
|
|
309
|
+
.health-dot::after {
|
|
310
|
+
content: ''; position: absolute; inset: -3px;
|
|
311
|
+
border-radius: 50%; animation: healthPulse 2s ease-in-out infinite;
|
|
312
|
+
}
|
|
313
|
+
.health-dot.healthy { background: var(--accent-green); }
|
|
314
|
+
.health-dot.healthy::after { border: 2px solid rgba(16,185,129,0.3); }
|
|
315
|
+
.health-dot.degraded { background: var(--accent-amber); }
|
|
316
|
+
.health-dot.degraded::after { border: 2px solid rgba(245,158,11,0.3); }
|
|
317
|
+
.health-dot.unhealthy { background: var(--accent-rose); }
|
|
318
|
+
.health-dot.unhealthy::after { border: 2px solid rgba(244,63,94,0.3); }
|
|
319
|
+
.health-dot.unknown { background: var(--text-muted); }
|
|
320
|
+
.health-dot.unknown::after { border: 2px solid rgba(100,116,139,0.3); }
|
|
321
|
+
@keyframes healthPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
322
|
+
.health-label { font-size: 0.8rem; font-weight: 500; }
|
|
323
|
+
.health-summary { font-size: 0.75rem; color: var(--text-muted); }
|
|
324
|
+
.health-issues { font-size: 0.8rem; color: var(--text-secondary); margin-top: 0.5rem; }
|
|
325
|
+
.health-issues .issue-row {
|
|
326
|
+
padding: 0.3rem 0; display: flex; gap: 0.5rem; align-items: flex-start;
|
|
327
|
+
}
|
|
328
|
+
.cleanup-btn {
|
|
329
|
+
margin-left: auto; background: rgba(244,63,94,0.12); border: 1px solid rgba(244,63,94,0.3);
|
|
330
|
+
color: var(--accent-rose); cursor: pointer; font-size: 0.75rem; font-weight: 600;
|
|
331
|
+
padding: 0.2rem 0.65rem; border-radius: 6px; transition: all 0.2s;
|
|
332
|
+
}
|
|
333
|
+
.cleanup-btn:hover { background: rgba(244,63,94,0.25); border-color: var(--accent-rose); }
|
|
334
|
+
.cleanup-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
335
|
+
/* ─── Health repair progress bar ─── */
|
|
336
|
+
.health-progress-wrap {
|
|
337
|
+
display: none; margin-top: 0.75rem;
|
|
338
|
+
background: rgba(15,23,42,0.7); border-radius: 8px;
|
|
339
|
+
padding: 0.65rem 0.85rem;
|
|
340
|
+
border: 1px solid rgba(139,92,246,0.25);
|
|
341
|
+
}
|
|
342
|
+
.health-progress-header {
|
|
343
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
344
|
+
margin-bottom: 0.45rem;
|
|
345
|
+
}
|
|
346
|
+
.health-progress-stage {
|
|
347
|
+
font-size: 0.72rem; color: #a78bfa; font-weight: 500;
|
|
348
|
+
transition: color 0.3s;
|
|
349
|
+
}
|
|
350
|
+
.health-progress-pct {
|
|
351
|
+
font-size: 0.72rem; color: var(--text-muted); font-weight: 600; font-variant-numeric: tabular-nums;
|
|
352
|
+
}
|
|
353
|
+
.health-progress-track {
|
|
354
|
+
height: 6px; border-radius: 3px;
|
|
355
|
+
background: rgba(139,92,246,0.15);
|
|
356
|
+
overflow: hidden;
|
|
357
|
+
}
|
|
358
|
+
.health-progress-bar {
|
|
359
|
+
height: 100%; width: 0%; border-radius: 3px;
|
|
360
|
+
background: linear-gradient(90deg, #7c3aed, #a78bfa, #7c3aed);
|
|
361
|
+
background-size: 200% 100%;
|
|
362
|
+
animation: healthBarShimmer 1.8s linear infinite;
|
|
363
|
+
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
364
|
+
}
|
|
365
|
+
@keyframes healthBarShimmer {
|
|
366
|
+
0% { background-position: 200% 0; }
|
|
367
|
+
100% { background-position: -200% 0; }
|
|
368
|
+
}
|
|
369
|
+
.health-progress-bar.done {
|
|
370
|
+
animation: none;
|
|
371
|
+
background: var(--accent-green);
|
|
372
|
+
transition: width 0.25s ease-out, background 0.3s;
|
|
373
|
+
}
|
|
374
|
+
.toast-fixed {
|
|
375
|
+
position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 200;
|
|
376
|
+
padding: 0.65rem 1.2rem; border-radius: 10px; font-size: 0.85rem; font-weight: 500;
|
|
377
|
+
backdrop-filter: blur(10px); border: 1px solid var(--border-glow);
|
|
378
|
+
background: var(--bg-secondary); color: var(--text-primary);
|
|
379
|
+
opacity: 0; transition: opacity 0.3s; pointer-events: none;
|
|
380
|
+
}
|
|
381
|
+
.toast-fixed.show { opacity: 1; }
|
|
382
|
+
|
|
383
|
+
/* ─── Neural Graph (v2.3.0) ─── */
|
|
384
|
+
#network-container {
|
|
385
|
+
width: 100%; height: 300px;
|
|
386
|
+
border-radius: var(--radius);
|
|
387
|
+
background: rgba(0,0,0,0.2);
|
|
388
|
+
border: 1px solid var(--border-glass);
|
|
389
|
+
}
|
|
390
|
+
.refresh-btn {
|
|
391
|
+
margin-left: auto; background: none; border: none;
|
|
392
|
+
color: var(--text-muted); cursor: pointer; font-size: 0.85rem;
|
|
393
|
+
transition: color 0.2s;
|
|
394
|
+
}
|
|
395
|
+
.refresh-btn:hover { color: var(--accent-purple); }
|
|
396
|
+
|
|
397
|
+
/* ─── Settings Modal (v3.0) ─── */
|
|
398
|
+
.settings-btn {
|
|
399
|
+
background: none; border: 1px solid var(--border-glass);
|
|
400
|
+
color: var(--text-secondary); cursor: pointer; font-size: 1.1rem;
|
|
401
|
+
padding: 0.4rem 0.7rem; border-radius: var(--radius-sm);
|
|
402
|
+
transition: all 0.2s;
|
|
403
|
+
}
|
|
404
|
+
.settings-btn:hover { border-color: var(--border-glow); color: var(--accent-purple); }
|
|
405
|
+
.identity-chip {
|
|
406
|
+
display: none; align-items: center; gap: 0.4rem;
|
|
407
|
+
padding: 0.35rem 0.75rem; border-radius: 999px;
|
|
408
|
+
background: rgba(139,92,246,0.12); border: 1px solid rgba(139,92,246,0.25);
|
|
409
|
+
color: var(--text-secondary); font-size: 0.8rem; font-weight: 500;
|
|
410
|
+
cursor: pointer; transition: all 0.2s;
|
|
411
|
+
}
|
|
412
|
+
.identity-chip:hover { border-color: var(--accent-purple); color: var(--accent-purple); background: rgba(139,92,246,0.2); }
|
|
413
|
+
.identity-chip .role-icon { font-size: 0.9rem; }
|
|
414
|
+
.identity-chip .identity-label { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
415
|
+
/* Settings modal tab bar */
|
|
416
|
+
.settings-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border-glass); margin: 0 -1.5rem 1.2rem; padding: 0 1.5rem; }
|
|
417
|
+
.s-tab { padding: 0.55rem 1.1rem; font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); cursor: pointer;
|
|
418
|
+
border-bottom: 2px solid transparent; transition: all 0.2s; background: none; border-top: none; border-left: none; border-right: none; }
|
|
419
|
+
.s-tab.active { color: var(--accent-purple); border-bottom-color: var(--accent-purple); }
|
|
420
|
+
.s-tab:hover:not(.active) { color: var(--text-primary); }
|
|
421
|
+
.s-tab-panel { display: none; } .s-tab-panel.active { display: block; }
|
|
422
|
+
/* Skills editor */
|
|
423
|
+
.skill-role-row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; }
|
|
424
|
+
.skill-role-row label { font-size: 0.82rem; color: var(--text-secondary); }
|
|
425
|
+
.skill-role-select { padding: 0.3rem 0.6rem; background: var(--bg-hover); color: var(--text-primary);
|
|
426
|
+
border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); }
|
|
427
|
+
.skill-textarea { width: 100%; min-height: 220px; background: var(--bg-hover); color: var(--text-primary);
|
|
428
|
+
border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 0.75rem;
|
|
429
|
+
font-size: 0.82rem; font-family: var(--font-mono); line-height: 1.5; resize: vertical;
|
|
430
|
+
box-sizing: border-box; transition: border-color 0.2s; }
|
|
431
|
+
.skill-textarea:focus { outline: none; border-color: var(--accent-purple); }
|
|
432
|
+
.skill-char-count { font-size: 0.74rem; color: var(--text-muted); text-align: right; margin-top: 0.3rem; }
|
|
433
|
+
.skill-actions { display: flex; gap: 0.6rem; margin-top: 0.85rem; align-items: center; }
|
|
434
|
+
.skill-save-btn { background: var(--accent-purple); color: #fff; border: none; border-radius: var(--radius-sm);
|
|
435
|
+
padding: 0.45rem 1rem; font-size: 0.82rem; font-weight: 600; cursor: pointer; transition: opacity 0.2s; }
|
|
436
|
+
.skill-save-btn:hover { opacity: 0.85; }
|
|
437
|
+
.skill-upload-btn { background: none; border: 1px solid var(--border-glass); color: var(--text-secondary);
|
|
438
|
+
border-radius: var(--radius-sm); padding: 0.45rem 0.85rem; font-size: 0.82rem; cursor: pointer; transition: all 0.2s; }
|
|
439
|
+
.skill-upload-btn:hover { border-color: var(--accent-purple); color: var(--accent-purple); }
|
|
440
|
+
.skill-clear-btn { background: none; border: none; color: var(--text-muted); font-size: 0.8rem; cursor: pointer;
|
|
441
|
+
margin-left: auto; transition: color 0.2s; }
|
|
442
|
+
.skill-clear-btn:hover { color: #ef4444; }
|
|
443
|
+
.skill-hint { font-size: 0.78rem; color: var(--text-muted); margin-top: 0.6rem; line-height: 1.5; }
|
|
444
|
+
.modal-overlay {
|
|
445
|
+
display: none; position: fixed; inset: 0; z-index: 100;
|
|
446
|
+
background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
|
|
447
|
+
justify-content: center; align-items: center;
|
|
448
|
+
}
|
|
449
|
+
.modal-overlay.active { display: flex; }
|
|
450
|
+
.modal {
|
|
451
|
+
background: var(--bg-secondary); border: 1px solid var(--border-glow);
|
|
452
|
+
border-radius: var(--radius); padding: 2rem; width: 480px; max-width: 90vw;
|
|
453
|
+
max-height: 85vh; overflow-y: auto; position: relative;
|
|
454
|
+
}
|
|
455
|
+
.modal h2 { font-size: 1.1rem; margin-bottom: 1.5rem; display: flex; align-items: center; gap: 0.5rem; }
|
|
456
|
+
.modal-close {
|
|
457
|
+
position: absolute; top: 1rem; right: 1rem; background: none;
|
|
458
|
+
border: none; color: var(--text-muted); cursor: pointer; font-size: 1.25rem;
|
|
459
|
+
}
|
|
460
|
+
.modal-close:hover { color: var(--text-primary); }
|
|
461
|
+
.setting-row {
|
|
462
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
463
|
+
padding: 0.75rem 0; border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
464
|
+
}
|
|
465
|
+
.setting-row:last-child { border-bottom: none; }
|
|
466
|
+
.setting-label { font-size: 0.85rem; color: var(--text-secondary); }
|
|
467
|
+
.setting-desc { font-size: 0.7rem; color: var(--text-muted); margin-top: 0.2rem; }
|
|
468
|
+
.toggle {
|
|
469
|
+
position: relative; width: 44px; height: 24px;
|
|
470
|
+
background: rgba(100,116,139,0.3); border-radius: 12px;
|
|
471
|
+
cursor: pointer; transition: background 0.3s; flex-shrink: 0;
|
|
472
|
+
}
|
|
473
|
+
.toggle.active { background: var(--accent-purple); }
|
|
474
|
+
.toggle::after {
|
|
475
|
+
content: ''; position: absolute; top: 2px; left: 2px;
|
|
476
|
+
width: 20px; height: 20px; border-radius: 50%;
|
|
477
|
+
background: white; transition: transform 0.3s;
|
|
478
|
+
}
|
|
479
|
+
.toggle.active::after { transform: translateX(20px); }
|
|
480
|
+
.setting-select {
|
|
481
|
+
background: var(--bg-primary); border: 1px solid var(--border-glass);
|
|
482
|
+
color: var(--text-primary); padding: 0.4rem 0.6rem;
|
|
483
|
+
border-radius: 6px; font-size: 0.8rem; font-family: var(--font-sans);
|
|
484
|
+
}
|
|
485
|
+
.setting-section {
|
|
486
|
+
font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
|
|
487
|
+
letter-spacing: 0.1em; color: var(--accent-purple); margin: 1rem 0 0.5rem;
|
|
488
|
+
}
|
|
489
|
+
.setting-saved {
|
|
490
|
+
font-size: 0.75rem; color: var(--accent-green); opacity: 0;
|
|
491
|
+
transition: opacity 0.3s; margin-left: 0.5rem;
|
|
492
|
+
}
|
|
493
|
+
.setting-saved.show { opacity: 1; }
|
|
494
|
+
.boot-badge {
|
|
495
|
+
font-size: 0.6rem; padding: 0.15rem 0.5rem; border-radius: 4px;
|
|
496
|
+
background: rgba(245,158,11,0.15); color: var(--accent-amber);
|
|
497
|
+
font-weight: 600; text-transform: uppercase;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* ─── Hivemind Radar (v3.0) ─── */
|
|
501
|
+
.team-list { list-style: none; padding: 0; }
|
|
502
|
+
.team-item {
|
|
503
|
+
display: flex; align-items: center; gap: 0.75rem;
|
|
504
|
+
padding: 0.6rem 0; border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
505
|
+
font-size: 0.85rem;
|
|
506
|
+
}
|
|
507
|
+
.team-item:last-child { border-bottom: none; }
|
|
508
|
+
.team-role { font-weight: 600; color: var(--text-primary); min-width: 60px; }
|
|
509
|
+
.team-task { color: var(--text-secondary); flex: 1; }
|
|
510
|
+
.team-heartbeat { font-size: 0.7rem; color: var(--text-muted); font-family: var(--font-mono); }
|
|
511
|
+
.pulse-dot {
|
|
512
|
+
width: 8px; height: 8px; border-radius: 50%; background: var(--accent-green);
|
|
513
|
+
flex-shrink: 0; animation: pulseDot 2s ease-in-out infinite;
|
|
514
|
+
}
|
|
515
|
+
.pulse-dot.looping {
|
|
516
|
+
animation: spinDot 1s linear infinite;
|
|
517
|
+
background: #a855f7 !important;
|
|
518
|
+
border-radius: 2px;
|
|
519
|
+
}
|
|
520
|
+
@keyframes pulseDot { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
521
|
+
@keyframes spinDot { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
522
|
+
.team-status { font-size: 0.8rem; flex-shrink: 0; }
|
|
523
|
+
|
|
524
|
+
/* ─── Memory Analytics (v3.1) ─── */
|
|
525
|
+
.sparkline {
|
|
526
|
+
display: flex; align-items: flex-end; gap: 3px;
|
|
527
|
+
height: 48px; margin: 0.75rem 0 0.25rem;
|
|
528
|
+
}
|
|
529
|
+
.spark-bar {
|
|
530
|
+
flex: 1; background: rgba(139,92,246,0.35);
|
|
531
|
+
border-radius: 3px 3px 0 0; min-height: 3px;
|
|
532
|
+
transition: background 0.2s;
|
|
533
|
+
}
|
|
534
|
+
.spark-bar:hover { background: var(--accent-purple); }
|
|
535
|
+
.analytics-stats {
|
|
536
|
+
display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;
|
|
537
|
+
margin-top: 0.75rem;
|
|
538
|
+
}
|
|
539
|
+
.astat {
|
|
540
|
+
background: rgba(15,23,42,0.5); border-radius: var(--radius-sm);
|
|
541
|
+
padding: 0.6rem 0.75rem; display: flex; flex-direction: column; gap: 0.15rem;
|
|
542
|
+
}
|
|
543
|
+
.astat-val { font-size: 1.1rem; font-weight: 700; color: var(--accent-purple); font-family: var(--font-mono); }
|
|
544
|
+
.astat-label { font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
545
|
+
|
|
546
|
+
/* ─── Lifecycle Controls (v3.1) ─── */
|
|
547
|
+
.lc-row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; align-items: center; }
|
|
548
|
+
.lc-btn {
|
|
549
|
+
flex: 1; padding: 0.5rem 0.6rem; font-size: 0.8rem; font-weight: 600;
|
|
550
|
+
border-radius: var(--radius-sm); border: none; cursor: pointer;
|
|
551
|
+
transition: all 0.2s; display: flex; align-items: center; justify-content: center; gap: 0.4rem;
|
|
552
|
+
}
|
|
553
|
+
.lc-btn.compact { background: rgba(139,92,246,0.15); color: var(--accent-purple); border: 1px solid rgba(139,92,246,0.3); }
|
|
554
|
+
.lc-btn.compact:hover { background: rgba(139,92,246,0.3); }
|
|
555
|
+
.lc-btn.export { background: rgba(16,185,129,0.12); color: var(--accent-green); border: 1px solid rgba(16,185,129,0.3); }
|
|
556
|
+
.lc-btn.export:hover { background: rgba(16,185,129,0.25); }
|
|
557
|
+
.lc-btn.export-vault { background: rgba(139,92,246,0.12); color: #a78bfa; border: 1px solid rgba(139,92,246,0.3); }
|
|
558
|
+
.lc-btn.export-vault:hover { background: rgba(139,92,246,0.25); }
|
|
559
|
+
.lc-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
560
|
+
.ttl-row { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem; }
|
|
561
|
+
.ttl-input {
|
|
562
|
+
width: 70px; background: var(--bg-secondary); border: 1px solid var(--border-glass);
|
|
563
|
+
color: var(--text-primary); border-radius: 6px; padding: 0.3rem 0.5rem;
|
|
564
|
+
font-size: 0.82rem; font-family: var(--font-mono); text-align: center;
|
|
565
|
+
}
|
|
566
|
+
.ttl-label { font-size: 0.8rem; color: var(--text-secondary); }
|
|
567
|
+
.ttl-save-btn {
|
|
568
|
+
margin-left: auto; padding: 0.3rem 0.75rem; font-size: 0.78rem; font-weight: 600;
|
|
569
|
+
background: rgba(245,158,11,0.15); color: var(--accent-amber);
|
|
570
|
+
border: 1px solid rgba(245,158,11,0.3); border-radius: 6px; cursor: pointer; transition: all 0.2s;
|
|
571
|
+
}
|
|
572
|
+
.ttl-save-btn:hover { background: rgba(245,158,11,0.3); }
|
|
573
|
+
.node-editor-panel {
|
|
574
|
+
background: var(--bg-card);
|
|
575
|
+
border: 1px solid var(--border-glow);
|
|
576
|
+
border-radius: 8px;
|
|
577
|
+
padding: 1rem;
|
|
578
|
+
margin-top: 1rem;
|
|
579
|
+
display: none;
|
|
580
|
+
}
|
|
581
|
+
</style>
|
|
582
|
+
</head>
|
|
583
|
+
<body>
|
|
584
|
+
<div class="bg-grid"></div>
|
|
585
|
+
<div class="container">
|
|
586
|
+
<header>
|
|
587
|
+
<div class="logo">
|
|
588
|
+
<span class="logo-icon">🧠</span>
|
|
589
|
+
Prism Mind Palace
|
|
590
|
+
<span class="version-badge">v${version}</span>
|
|
591
|
+
</div>
|
|
592
|
+
<div class="selector">
|
|
593
|
+
<span class="identity-chip" id="identityChip" onclick="openSettings()" title="Agent Identity — click to change"></span>
|
|
594
|
+
<select id="projectSelect">
|
|
595
|
+
<option value="">Loading projects...</option>
|
|
596
|
+
</select>
|
|
597
|
+
<button onclick="loadProject()">Inspect</button>
|
|
598
|
+
<button class="settings-btn" onclick="openSettings()" title="Settings">⚙️</button>
|
|
599
|
+
</div>
|
|
600
|
+
</header>
|
|
601
|
+
|
|
602
|
+
<div class="main-tabs" style="display:flex; gap: 1rem; border-bottom: 1px solid var(--border-glass); margin-bottom: 1.5rem; padding-bottom: 0;">
|
|
603
|
+
<button class="s-tab active" id="mtab-project" onclick="switchMainTab('project')" style="font-size: 1rem;">📁 Project View</button>
|
|
604
|
+
<button class="s-tab" id="mtab-search" onclick="switchMainTab('search')" style="font-size: 1rem;">🔍 Vector Search</button>
|
|
605
|
+
<button class="s-tab" id="mtab-factory" onclick="switchMainTab('factory')" style="font-size: 1rem;">🏭 Factory</button>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<div id="welcome" class="empty">
|
|
609
|
+
<div class="emoji">🔮</div>
|
|
610
|
+
<p>Select a project to inspect its neural state.</p>
|
|
611
|
+
</div>
|
|
612
|
+
|
|
613
|
+
<div id="loading" class="loading">
|
|
614
|
+
<span class="spinner"></span> Neural link establishing...
|
|
615
|
+
</div>
|
|
616
|
+
|
|
617
|
+
<div id="content" class="grid grid-main fade-in">
|
|
618
|
+
<!-- Left Column -->
|
|
619
|
+
<div class="grid" style="align-content: start;">
|
|
620
|
+
<!-- Current State -->
|
|
621
|
+
<div class="card">
|
|
622
|
+
<div class="card-title"><span class="dot" style="background:var(--accent-blue)"></span> Current State <span id="versionBadge" class="badge badge-purple" style="margin-left:auto"></span></div>
|
|
623
|
+
<div class="summary-text" id="summary"></div>
|
|
624
|
+
<div class="card-title" style="margin-top:0.5rem"><span class="dot" style="background:var(--accent-cyan)"></span> Pending TODOs</div>
|
|
625
|
+
<ul class="todo-list" id="todos"></ul>
|
|
626
|
+
</div>
|
|
627
|
+
|
|
628
|
+
<!-- Git Metadata -->
|
|
629
|
+
<div class="card">
|
|
630
|
+
<div class="card-title"><span class="dot" style="background:var(--accent-green)"></span> Git Metadata</div>
|
|
631
|
+
<div class="git-row"><span class="git-label">Branch</span><span class="git-value" id="gitBranch">—</span></div>
|
|
632
|
+
<div class="git-row"><span class="git-label">Commit</span><span class="git-value" id="gitSha">—</span></div>
|
|
633
|
+
<div class="git-row"><span class="git-label">Key Context</span><span class="git-value" id="keyContext" style="font-family:var(--font-sans);max-width:200px;text-align:right">—</span></div>
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
<!-- Brain Health (v2.2.0) -->
|
|
637
|
+
<div class="card" id="healthCard" style="display:none">
|
|
638
|
+
<div class="card-title">
|
|
639
|
+
<span class="dot" style="background:var(--accent-green)"></span> Brain Health 🩺
|
|
640
|
+
<button class="cleanup-btn" id="cleanupBtn" onclick="cleanupIssues()" style="display:none">🧹 Fix Issues</button>
|
|
641
|
+
</div>
|
|
642
|
+
<div class="health-status">
|
|
643
|
+
<div class="health-dot unknown" id="healthDot"></div>
|
|
644
|
+
<div>
|
|
645
|
+
<div class="health-label" id="healthLabel">Scanning...</div>
|
|
646
|
+
<div class="health-summary" id="healthSummary"></div>
|
|
647
|
+
</div>
|
|
648
|
+
</div>
|
|
649
|
+
<div class="health-issues" id="healthIssues"></div>
|
|
650
|
+
<!-- Repair progress bar (v6.1.4) -->
|
|
651
|
+
<div class="health-progress-wrap" id="healthProgressWrap">
|
|
652
|
+
<div class="health-progress-header">
|
|
653
|
+
<span class="health-progress-stage" id="healthProgressStage">Initializing…</span>
|
|
654
|
+
<span class="health-progress-pct" id="healthProgressPct">0%</span>
|
|
655
|
+
</div>
|
|
656
|
+
<div class="health-progress-track">
|
|
657
|
+
<div class="health-progress-bar" id="healthProgressBar"></div>
|
|
658
|
+
</div>
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
|
|
662
|
+
<!-- Memory Analytics (v3.1) -->
|
|
663
|
+
<div class="card" id="analyticsCard" style="display:none">
|
|
664
|
+
<div class="card-title">
|
|
665
|
+
<span class="dot" style="background:var(--accent-purple)"></span>
|
|
666
|
+
Memory Analytics 📊
|
|
667
|
+
</div>
|
|
668
|
+
<div class="sparkline" id="sparkline" title="Sessions per day (last 14 days)"></div>
|
|
669
|
+
<div style="font-size:0.68rem;color:var(--text-muted);text-align:right">Sessions / day (14d)</div>
|
|
670
|
+
<div class="analytics-stats">
|
|
671
|
+
<div class="astat"><div class="astat-val" id="astat-entries">—</div><div class="astat-label">Active sessions</div></div>
|
|
672
|
+
<div class="astat"><div class="astat-val" id="astat-rollups">—</div><div class="astat-label">Rollups</div></div>
|
|
673
|
+
<div class="astat"><div class="astat-val" id="astat-savings">—</div><div class="astat-label">Entries saved</div></div>
|
|
674
|
+
<div class="astat"><div class="astat-val" id="astat-avglen">—</div><div class="astat-label">Avg summary chars</div></div>
|
|
675
|
+
</div>
|
|
676
|
+
</div>
|
|
677
|
+
|
|
678
|
+
<!-- Lifecycle Controls (v3.1) -->
|
|
679
|
+
<div class="card" id="lifecycleCard" style="display:none">
|
|
680
|
+
<div class="card-title"><span class="dot" style="background:var(--accent-amber)"></span> Lifecycle Controls ⚙️</div>
|
|
681
|
+
<div class="lc-row">
|
|
682
|
+
<button class="lc-btn compact" id="compactBtn" onclick="compactNow()">
|
|
683
|
+
🗜️ Compact Now
|
|
684
|
+
</button>
|
|
685
|
+
<button class="lc-btn export" id="exportBtn" onclick="exportPKM()">
|
|
686
|
+
📦 Export ZIP
|
|
687
|
+
</button>
|
|
688
|
+
<button class="lc-btn export-vault" id="exportVaultBtn" onclick="exportVault()" title="Export as Obsidian/Logseq-compatible vault with Wikilinks and keyword index">
|
|
689
|
+
🏛️ Export Vault
|
|
690
|
+
</button>
|
|
691
|
+
</div>
|
|
692
|
+
<!-- Export progress bar (v6.1.4) -->
|
|
693
|
+
<div class="health-progress-wrap" id="exportProgressWrap">
|
|
694
|
+
<div class="health-progress-header">
|
|
695
|
+
<span class="health-progress-stage" id="exportProgressStage">Building archive…</span>
|
|
696
|
+
<span class="health-progress-pct" id="exportProgressPct">0%</span>
|
|
697
|
+
</div>
|
|
698
|
+
<div class="health-progress-track">
|
|
699
|
+
<div class="health-progress-bar" id="exportProgressBar"></div>
|
|
700
|
+
</div>
|
|
701
|
+
</div>
|
|
702
|
+
<div class="ttl-row">
|
|
703
|
+
<span class="ttl-label">Auto-expire after</span>
|
|
704
|
+
<input type="number" class="ttl-input" id="ttlInput" min="0" max="3650" placeholder="0" title="Days. 0 = disabled">
|
|
705
|
+
<span class="ttl-label">days</span>
|
|
706
|
+
<button class="ttl-save-btn" onclick="saveTTL()">Save TTL</button>
|
|
707
|
+
</div>
|
|
708
|
+
<div style="font-size:0.7rem;color:var(--text-muted);margin-top:0.4rem">0 = disabled. Min 7 days. Rollups are never expired.</div>
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
<!-- Universal History Import (v5.2) -->
|
|
712
|
+
<div class="card" id="importCard" style="display:none">
|
|
713
|
+
<div class="card-title"><span class="dot" style="background:var(--accent-cyan)"></span> Import History 📥</div>
|
|
714
|
+
<div style="margin-bottom:0.75rem">
|
|
715
|
+
<label style="font-size:0.78rem;color:var(--text-muted);display:block;margin-bottom:0.3rem">Source File</label>
|
|
716
|
+
<div style="display:flex;gap:0.4rem;align-items:center">
|
|
717
|
+
<input type="text" id="importPath" class="ttl-input" style="flex:1;text-align:left;font-size:0.82rem;padding:0.45rem 0.65rem" placeholder="/path/to/conversations.jsonl">
|
|
718
|
+
<input type="file" id="importFileInput" accept=".jsonl,.json,.ndjson" style="display:none">
|
|
719
|
+
<button class="lc-btn compact" onclick="document.getElementById('importFileInput').click()" style="flex:none;padding:0.45rem 0.75rem;font-size:0.82rem;white-space:nowrap" title="Choose a file from your computer">
|
|
720
|
+
📂 Browse
|
|
721
|
+
</button>
|
|
722
|
+
<button class="lc-btn" onclick="clearImportFile()" id="importClearBtn" style="flex:none;padding:0.45rem 0.55rem;font-size:0.82rem;display:none;background:rgba(244,63,94,0.15);border-color:rgba(244,63,94,0.3);color:var(--accent-rose)" title="Clear selection">
|
|
723
|
+
✕
|
|
724
|
+
</button>
|
|
725
|
+
</div>
|
|
726
|
+
<div id="importFileInfo" style="display:none;margin-top:0.35rem;font-size:0.72rem;color:var(--accent-cyan)"></div>
|
|
727
|
+
</div>
|
|
728
|
+
<div style="display:flex;gap:0.5rem;margin-bottom:0.75rem;flex-wrap:wrap">
|
|
729
|
+
<div style="flex:1;min-width:120px">
|
|
730
|
+
<label style="font-size:0.78rem;color:var(--text-muted);display:block;margin-bottom:0.3rem">Format</label>
|
|
731
|
+
<select id="importFormat" class="ttl-input" style="width:100%;text-align:left;font-size:0.82rem;padding:0.35rem 0.5rem;cursor:pointer">
|
|
732
|
+
<option value="">Auto-detect</option>
|
|
733
|
+
<option value="claude">Claude Code (.jsonl)</option>
|
|
734
|
+
<option value="gemini">Gemini (.json)</option>
|
|
735
|
+
<option value="openai">OpenAI (.json)</option>
|
|
736
|
+
</select>
|
|
737
|
+
</div>
|
|
738
|
+
<div style="flex:1;min-width:120px">
|
|
739
|
+
<label style="font-size:0.78rem;color:var(--text-muted);display:block;margin-bottom:0.3rem">Target Project</label>
|
|
740
|
+
<input type="text" id="importProject" class="ttl-input" style="width:100%;text-align:left;font-size:0.82rem;padding:0.45rem 0.65rem" placeholder="(auto from file)">
|
|
741
|
+
</div>
|
|
742
|
+
</div>
|
|
743
|
+
<div style="display:flex;gap:0.5rem;align-items:center">
|
|
744
|
+
<button class="lc-btn compact" id="importBtn" onclick="runImport(false)" style="flex:1">
|
|
745
|
+
📥 Import
|
|
746
|
+
</button>
|
|
747
|
+
<button class="lc-btn export" id="importDryBtn" onclick="runImport(true)" style="flex:1" title="Validate without writing to storage">
|
|
748
|
+
🧪 Dry Run
|
|
749
|
+
</button>
|
|
750
|
+
</div>
|
|
751
|
+
<div id="importResult" style="display:none;margin-top:0.75rem;padding:0.65rem 0.85rem;border-radius:var(--radius-sm);font-size:0.82rem;line-height:1.5"></div>
|
|
752
|
+
<!-- Import progress bar (v6.1.4) -->
|
|
753
|
+
<div class="health-progress-wrap" id="importProgressWrap">
|
|
754
|
+
<div class="health-progress-header">
|
|
755
|
+
<span class="health-progress-stage" id="importProgressStage">Reading file…</span>
|
|
756
|
+
<span class="health-progress-pct" id="importProgressPct">0%</span>
|
|
757
|
+
</div>
|
|
758
|
+
<div class="health-progress-track">
|
|
759
|
+
<div class="health-progress-bar" id="importProgressBar"></div>
|
|
760
|
+
</div>
|
|
761
|
+
</div>
|
|
762
|
+
<div style="font-size:0.68rem;color:var(--text-muted);margin-top:0.5rem">
|
|
763
|
+
Click <strong>Browse</strong> to pick a file, or type a server-side path.<br>
|
|
764
|
+
Supports Claude Code (.jsonl), Gemini (.json), and OpenAI (.json).
|
|
765
|
+
</div>
|
|
766
|
+
</div>
|
|
767
|
+
|
|
768
|
+
<div class="card" id="briefingCard" style="display:none">
|
|
769
|
+
<div class="card-title"><span class="dot" style="background:var(--accent-amber)"></span> Morning Briefing 🌅</div>
|
|
770
|
+
<div class="briefing-text" id="briefingText"></div>
|
|
771
|
+
</div>
|
|
772
|
+
|
|
773
|
+
<!-- Visual Memory -->
|
|
774
|
+
<div class="card" id="visualCard" style="display:none">
|
|
775
|
+
<div class="card-title"><span class="dot" style="background:var(--accent-rose)"></span> Visual Memory 🖼️</div>
|
|
776
|
+
<ul class="visual-list" id="visualList"></ul>
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
|
|
780
|
+
<!-- Right Column -->
|
|
781
|
+
<div class="grid" style="align-content: start;">
|
|
782
|
+
|
|
783
|
+
<!-- Neural Graph (v2.3.0 / v5.1) -->
|
|
784
|
+
<div class="card">
|
|
785
|
+
<div class="card-title">
|
|
786
|
+
<span class="dot" style="background:var(--accent-blue)"></span>
|
|
787
|
+
Neural Graph 🕸️
|
|
788
|
+
<button onclick="loadGraph()" class="refresh-btn">↻</button>
|
|
789
|
+
</div>
|
|
790
|
+
|
|
791
|
+
<!-- v5.1 Graph Filters -->
|
|
792
|
+
<div style="display:flex; gap:0.5rem; margin-bottom:1rem; flex-wrap:wrap; align-items:center;">
|
|
793
|
+
<select id="graphProjectFilter" class="input-modern" style="min-width:120px; font-size:0.75rem; padding:0.3rem 0.5rem" onchange="loadGraph()">
|
|
794
|
+
<option value="">All Projects</option>
|
|
795
|
+
</select>
|
|
796
|
+
<select id="graphDaysFilter" class="input-modern" style="font-size:0.75rem; padding:0.3rem 0.5rem" onchange="loadGraph()">
|
|
797
|
+
<option value="">All Time</option>
|
|
798
|
+
<option value="7">Last 7 Days</option>
|
|
799
|
+
<option value="30">Last 30 Days</option>
|
|
800
|
+
<option value="90">Last 90 Days</option>
|
|
801
|
+
</select>
|
|
802
|
+
<select id="graphImportanceFilter" class="input-modern" style="font-size:0.75rem; padding:0.3rem 0.5rem" onchange="loadGraph()">
|
|
803
|
+
<option value="">Any Importance</option>
|
|
804
|
+
<option value="5">Importance >= 5</option>
|
|
805
|
+
<option value="7">Graduated (>= 7)</option>
|
|
806
|
+
</select>
|
|
807
|
+
<button id="decayToggle" class="input-modern" style="font-size:0.75rem; padding:0.3rem 0.65rem; cursor:pointer; border:1px solid rgba(139,92,246,0.3); background:transparent; color:var(--text-secondary); border-radius:6px; transition:all 0.2s; white-space:nowrap" onclick="toggleDecayView()" title="Color nodes by temporal freshness (Ebbinghaus decay)">
|
|
808
|
+
🗓️ Decay View
|
|
809
|
+
</button>
|
|
810
|
+
</div>
|
|
811
|
+
|
|
812
|
+
<div id="network-container">Loading nodes...</div>
|
|
813
|
+
|
|
814
|
+
<!-- Graph Maintenance Actions -->
|
|
815
|
+
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border-glass); display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
816
|
+
<button onclick="triggerEdgeSynthesis()" class="input-modern" style="font-size:0.75rem; padding:0.3rem 0.65rem; cursor:pointer; border:1px solid var(--accent-purple); background:transparent; color:var(--text-primary); border-radius:6px; transition:all 0.2s;">
|
|
817
|
+
⚡ Synthesize Edges
|
|
818
|
+
</button>
|
|
819
|
+
<span id="synthesisStatus" style="font-size: 0.75rem; color: var(--text-muted); align-self: center;"></span>
|
|
820
|
+
</div>
|
|
821
|
+
<!-- v5.1 Node Editor Panel -->
|
|
822
|
+
<div id="nodeEditorPanel" class="node-editor-panel">
|
|
823
|
+
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.8rem;">
|
|
824
|
+
<h4 id="nodeEditorTitle" style="margin:0; font-size:0.9rem; color:var(--text-primary);">Node Name</h4>
|
|
825
|
+
<span id="nodeEditorGroup" class="badge">category</span>
|
|
826
|
+
</div>
|
|
827
|
+
|
|
828
|
+
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:0.3rem">Rename (Or leave empty to delete)</label>
|
|
829
|
+
<div style="display:flex; gap:0.5rem; margin-bottom:0.6rem;">
|
|
830
|
+
<input type="text" id="nodeEditorInput" class="input-modern" style="flex:1; font-size:0.8rem; padding:0.3rem 0.6rem" placeholder="New keyword name...">
|
|
831
|
+
<button onclick="submitNodeEdit()" class="btn-modern" style="padding:0.3rem 0.8rem; font-size:0.8rem">Apply</button>
|
|
832
|
+
<button onclick="document.getElementById('nodeEditorPanel').style.display='none'" class="btn-modern" style="background:transparent; border-color:var(--border-subtle); padding:0.3rem 0.8rem; font-size:0.8rem">Cancel</button>
|
|
833
|
+
</div>
|
|
834
|
+
|
|
835
|
+
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:0.3rem">Or merge into existing:</label>
|
|
836
|
+
<select id="nodeMergeSelect" class="input-modern" style="width:100%; font-size:0.8rem; padding:0.3rem 0.5rem" onchange="if(this.value) document.getElementById('nodeEditorInput').value = this.value">
|
|
837
|
+
<option value="">-- Select node --</option>
|
|
838
|
+
</select>
|
|
839
|
+
|
|
840
|
+
<hr style="border:none; border-top:1px solid var(--border-subtle); margin:1rem 0;">
|
|
841
|
+
<div style="display:flex; justify-content:space-between; align-items:center;">
|
|
842
|
+
<span style="font-size:0.75rem; color:var(--text-muted); font-weight:600;">Active Recall</span>
|
|
843
|
+
<button id="testMeBtn" onclick="triggerTestMe()" class="btn-modern" style="padding:0.3rem 0.6rem; font-size:0.75rem; background:var(--accent-teal); border-color:var(--accent-teal);" title="Generate 3 quiz questions using AI">📝 Test Me</button>
|
|
844
|
+
</div>
|
|
845
|
+
<div id="testMeContainer" style="margin-top:0.8rem; display:flex; flex-direction:column; gap:0.5rem;"></div>
|
|
846
|
+
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:0.8rem;">
|
|
847
|
+
<span style="font-size:0.75rem; color:var(--text-muted); font-weight:600;">Cognitive Route (v6.5)</span>
|
|
848
|
+
<button id="cognitiveRouteBtn" onclick="triggerCognitiveRoute()" class="btn-modern" style="padding:0.3rem 0.6rem; font-size:0.75rem; background:var(--accent-blue); border-color:var(--accent-blue);" title="Resolve concept route and explain why it surfaced">🧭 Route</button>
|
|
849
|
+
</div>
|
|
850
|
+
<div id="cognitiveRouteContainer" style="margin-top:0.6rem; display:flex; flex-direction:column; gap:0.4rem;"></div>
|
|
851
|
+
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
|
|
855
|
+
<!-- Time Travel -->
|
|
856
|
+
<div class="card">
|
|
857
|
+
<div class="card-title"><span class="dot" style="background:var(--accent-purple)"></span> Time Travel History 🕰️</div>
|
|
858
|
+
<div class="timeline" id="historyTimeline"></div>
|
|
859
|
+
</div>
|
|
860
|
+
|
|
861
|
+
<!-- Ledger -->
|
|
862
|
+
<div class="card">
|
|
863
|
+
<div class="card-title"><span class="dot" style="background:var(--accent-amber)"></span> Session Ledger</div>
|
|
864
|
+
<div class="timeline" id="ledgerTimeline"></div>
|
|
865
|
+
</div>
|
|
866
|
+
</div>
|
|
867
|
+
|
|
868
|
+
<!-- Hivemind Radar (v3.0) -->
|
|
869
|
+
<div class="card" id="hivemindCard" style="display:none">
|
|
870
|
+
<div class="card-title">
|
|
871
|
+
<span class="dot" style="background:var(--accent-cyan)"></span>
|
|
872
|
+
Hivemind Radar 🐝
|
|
873
|
+
<button onclick="loadTeam()" class="refresh-btn">↻</button>
|
|
874
|
+
</div>
|
|
875
|
+
<ul class="team-list" id="teamList">
|
|
876
|
+
<li style="color:var(--text-muted);font-size:0.85rem;text-align:center;padding:1rem">
|
|
877
|
+
No active agents. Set PRISM_ENABLE_HIVEMIND=true to enable.
|
|
878
|
+
</li>
|
|
879
|
+
</ul>
|
|
880
|
+
</div>
|
|
881
|
+
|
|
882
|
+
<!-- Background Scheduler Status (v5.4) -->
|
|
883
|
+
<div class="card" id="schedulerCard">
|
|
884
|
+
<div class="card-title" style="display:flex;align-items:center;">
|
|
885
|
+
<span class="dot" style="background:var(--accent-amber, #f59e0b)"></span>
|
|
886
|
+
Background Scheduler ⏰
|
|
887
|
+
<div style="flex:1"></div>
|
|
888
|
+
<button id="scholarBtn" onclick="triggerWebScholar()" class="lc-btn compact" style="margin-right:0.5rem">🧠 Scholar (Run)</button>
|
|
889
|
+
<button onclick="loadSchedulerStatus()" class="refresh-btn">↻</button>
|
|
890
|
+
</div>
|
|
891
|
+
<div id="schedulerContent" style="font-size:0.8rem;color:var(--text-muted)">
|
|
892
|
+
Loading scheduler status...
|
|
893
|
+
</div>
|
|
894
|
+
<div id="densityStatContainer" style="margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--border-glass); font-size: 0.85em; color: var(--text-muted); display:none">
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
|
|
899
|
+
<!-- Graph Health Metrics (v6.0 Observability) -->
|
|
900
|
+
<div class="card" style="margin-top:1rem">
|
|
901
|
+
<div class="card-title" style="display:flex;align-items:center;gap:0.5rem">
|
|
902
|
+
<span class="dot" style="background:var(--accent-blue)"></span>
|
|
903
|
+
Graph Health 📊
|
|
904
|
+
<div style="flex:1"></div>
|
|
905
|
+
<span id="graphHealthWarnings" style="display:inline-flex;gap:0.3rem"></span>
|
|
906
|
+
<button onclick="loadGraphMetrics()" class="refresh-btn">↻</button>
|
|
907
|
+
</div>
|
|
908
|
+
<div id="graphMetricsContent" style="font-size:0.8rem;color:var(--text-muted)">
|
|
909
|
+
Loading graph metrics...
|
|
910
|
+
</div>
|
|
911
|
+
</div>
|
|
912
|
+
</div>
|
|
913
|
+
|
|
914
|
+
<!-- Search View (v6.0) -->
|
|
915
|
+
<div id="search-content" class="fade-in" style="display:none; margin: 0 auto; max-width: 900px; padding: 0 1rem;">
|
|
916
|
+
<div class="card">
|
|
917
|
+
<div class="card-title"><span class="dot" style="background:var(--accent-purple)"></span> Semantic Vector Search 🔍</div>
|
|
918
|
+
<div style="margin-bottom: 1.5rem; display:flex; gap:1rem; align-items:center;">
|
|
919
|
+
<input type="text" id="searchInput" class="input-modern" style="flex:1; padding: 0.8rem 1rem; font-size: 1rem;" placeholder="Search past work, bugs, architecture decisions...">
|
|
920
|
+
<label style="font-size: 0.85rem; color: var(--text-secondary); display:flex; align-items:center; gap:0.4rem; cursor:pointer;" title="Biases results towards the currently scoped project">
|
|
921
|
+
<input type="checkbox" id="searchContextBoost" checked>
|
|
922
|
+
Context Boost
|
|
923
|
+
</label>
|
|
924
|
+
</div>
|
|
925
|
+
<div id="searchResults" class="timeline" style="margin-top: 1rem;">
|
|
926
|
+
<div style="color:var(--text-muted); font-size:0.9rem; padding: 2rem; text-align:center;">
|
|
927
|
+
Enter a query to search the neural ledger via embeddings...
|
|
928
|
+
</div>
|
|
929
|
+
</div>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
|
|
933
|
+
<!-- Dark Factory View (v7.3) -->
|
|
934
|
+
<div id="factory-content" class="fade-in" style="display:none; margin: 0 auto; max-width: 1000px; padding: 0 1rem;">
|
|
935
|
+
<div class="card">
|
|
936
|
+
<div class="card-title" style="display:flex;align-items:center;">
|
|
937
|
+
<span class="dot" style="background:var(--accent-amber)"></span>
|
|
938
|
+
Dark Factory — Autonomous Pipelines 🏭
|
|
939
|
+
<div style="flex:1"></div>
|
|
940
|
+
<button onclick="loadPipelines()" class="refresh-btn">↻</button>
|
|
941
|
+
</div>
|
|
942
|
+
<div style="display:flex;gap:0.5rem;margin-bottom:1rem;flex-wrap:wrap;align-items:center;">
|
|
943
|
+
<select id="factoryStatusFilter" class="input-modern" style="font-size:0.75rem;padding:0.3rem 0.5rem" onchange="loadPipelines()">
|
|
944
|
+
<option value="">All Statuses</option>
|
|
945
|
+
<option value="PENDING">⏸ Pending</option>
|
|
946
|
+
<option value="RUNNING">⏳ Running</option>
|
|
947
|
+
<option value="COMPLETED">✅ Completed</option>
|
|
948
|
+
<option value="FAILED">❌ Failed</option>
|
|
949
|
+
<option value="ABORTED">🛑 Aborted</option>
|
|
950
|
+
</select>
|
|
951
|
+
<span id="factoryCount" style="font-size:0.75rem;color:var(--text-muted);margin-left:auto"></span>
|
|
952
|
+
</div>
|
|
953
|
+
<div id="factoryList" style="font-size:0.85rem;color:var(--text-muted)">Loading pipelines...</div>
|
|
954
|
+
</div>
|
|
955
|
+
</div>
|
|
956
|
+
|
|
957
|
+
<!-- Settings Modal (v3.0) -->
|
|
958
|
+
<div class="modal-overlay" id="settingsModal">
|
|
959
|
+
<div class="modal">
|
|
960
|
+
<button class="modal-close" onclick="closeSettings()">✕</button>
|
|
961
|
+
<h2>⚙️ Settings</h2>
|
|
962
|
+
|
|
963
|
+
<!-- Tab bar -->
|
|
964
|
+
<div class="settings-tabs">
|
|
965
|
+
<button class="s-tab active" id="stab-settings" onclick="switchSettingsTab('settings')">⚙️ Settings</button>
|
|
966
|
+
<button class="s-tab" id="stab-skills" onclick="switchSettingsTab('skills')">📜 Skills</button>
|
|
967
|
+
<button class="s-tab" id="stab-providers" onclick="switchSettingsTab('providers')">🤖 AI Providers</button>
|
|
968
|
+
<button class="s-tab" id="stab-observability" onclick="switchSettingsTab('observability')">🔭 Observability</button>
|
|
969
|
+
</div>
|
|
970
|
+
|
|
971
|
+
<!-- Settings panel (existing content) -->
|
|
972
|
+
<div class="s-tab-panel active" id="spanel-settings">
|
|
973
|
+
|
|
974
|
+
<div class="setting-section">Runtime Settings</div>
|
|
975
|
+
|
|
976
|
+
<div class="setting-row">
|
|
977
|
+
<div>
|
|
978
|
+
<div class="setting-label">Auto-Capture HTML</div>
|
|
979
|
+
<div class="setting-desc">Capture local dev server UI on handoff save</div>
|
|
980
|
+
</div>
|
|
981
|
+
<div class="toggle" id="toggle-auto-capture" onclick="toggleSetting('auto_capture', this)"></div>
|
|
982
|
+
</div>
|
|
983
|
+
|
|
984
|
+
<div class="setting-row">
|
|
985
|
+
<div>
|
|
986
|
+
<div class="setting-label">Dashboard Theme</div>
|
|
987
|
+
<div class="setting-desc">Visual theme for Mind Palace</div>
|
|
988
|
+
</div>
|
|
989
|
+
<select class="setting-select" id="select-theme" onchange="saveSetting('dashboard_theme', this.value)">
|
|
990
|
+
<option value="dark">Dark (Default)</option>
|
|
991
|
+
<option value="midnight">Midnight</option>
|
|
992
|
+
<option value="purple">Purple Haze</option>
|
|
993
|
+
</select>
|
|
994
|
+
</div>
|
|
995
|
+
|
|
996
|
+
<div class="setting-row">
|
|
997
|
+
<div>
|
|
998
|
+
<div class="setting-label">Context Depth</div>
|
|
999
|
+
<div class="setting-desc">Default level for session_load_context</div>
|
|
1000
|
+
</div>
|
|
1001
|
+
<select class="setting-select" id="select-context-depth" onchange="saveSetting('default_context_depth', this.value)">
|
|
1002
|
+
<option value="standard">Standard (~200 tokens)</option>
|
|
1003
|
+
<option value="quick">Quick (~50 tokens)</option>
|
|
1004
|
+
<option value="deep">Deep (~1000+ tokens)</option>
|
|
1005
|
+
</select>
|
|
1006
|
+
</div>
|
|
1007
|
+
|
|
1008
|
+
<div class="setting-row">
|
|
1009
|
+
<div>
|
|
1010
|
+
<div class="setting-label">Token Budget</div>
|
|
1011
|
+
<div class="setting-desc">Max tokens for session_load_context (0 = unlimited)</div>
|
|
1012
|
+
</div>
|
|
1013
|
+
<input type="number" id="input-max-tokens"
|
|
1014
|
+
placeholder="0"
|
|
1015
|
+
min="0" max="100000" step="500"
|
|
1016
|
+
style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 90px; text-align: right;"
|
|
1017
|
+
onchange="saveSetting('max_tokens', this.value)"
|
|
1018
|
+
oninput="clearTimeout(this._t); var _s=this; this._t=setTimeout(function(){saveSetting('max_tokens',_s.value)},800)" />
|
|
1019
|
+
</div>
|
|
1020
|
+
|
|
1021
|
+
<div class="setting-section">Boot Settings <span class="boot-badge">Restart Required</span></div>
|
|
1022
|
+
|
|
1023
|
+
<div class="setting-row">
|
|
1024
|
+
<div>
|
|
1025
|
+
<div class="setting-label">Hivemind Mode</div>
|
|
1026
|
+
<div class="setting-desc">Multi-agent coordination (PRISM_ENABLE_HIVEMIND)</div>
|
|
1027
|
+
</div>
|
|
1028
|
+
<div class="toggle" id="toggle-hivemind" onclick="toggleBootSetting('hivemind_enabled', this)"></div>
|
|
1029
|
+
</div>
|
|
1030
|
+
<div class="setting-row">
|
|
1031
|
+
<div>
|
|
1032
|
+
<div class="setting-label">Task Router</div>
|
|
1033
|
+
<div class="setting-desc">Route tasks to local Claw agent (PRISM_TASK_ROUTER_ENABLED)</div>
|
|
1034
|
+
</div>
|
|
1035
|
+
<div class="toggle" id="toggle-task-router" onclick="toggleBootSetting('task_router_enabled', this)"></div>
|
|
1036
|
+
</div>
|
|
1037
|
+
<div class="setting-row">
|
|
1038
|
+
<div>
|
|
1039
|
+
<div class="setting-label">Storage Backend</div>
|
|
1040
|
+
<div class="setting-desc">Switch between SQLite and Supabase</div>
|
|
1041
|
+
</div>
|
|
1042
|
+
<select id="storageBackendSelect" onchange="window.saveBootSetting('PRISM_STORAGE', this.value)" style="padding: 0.2rem 0.4rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); cursor: pointer;">
|
|
1043
|
+
<option value="local">SQLite</option>
|
|
1044
|
+
<option value="supabase">Supabase</option>
|
|
1045
|
+
</select>
|
|
1046
|
+
</div>
|
|
1047
|
+
|
|
1048
|
+
<div class="setting-row" style="align-items:flex-start">
|
|
1049
|
+
<div>
|
|
1050
|
+
<div class="setting-label">Auto-Load Projects</div>
|
|
1051
|
+
<div class="setting-desc">Select projects to auto-push context on startup</div>
|
|
1052
|
+
</div>
|
|
1053
|
+
<div id="autoload-checkboxes" style="display:flex;flex-direction:column;gap:4px;font-size:0.85rem;font-family:var(--font-mono);max-height:120px;overflow-y:auto;">
|
|
1054
|
+
<span style="color:var(--text-muted);font-size:0.8rem">Loading…</span>
|
|
1055
|
+
</div>
|
|
1056
|
+
</div>
|
|
1057
|
+
|
|
1058
|
+
<div class="setting-row" style="align-items:flex-start">
|
|
1059
|
+
<div>
|
|
1060
|
+
<div class="setting-label">Project Repo Paths</div>
|
|
1061
|
+
<div class="setting-desc">Map each project to its repo directory for save validation</div>
|
|
1062
|
+
</div>
|
|
1063
|
+
<div id="repopath-inputs" style="display:flex;flex-direction:column;gap:6px;font-size:0.85rem;max-height:160px;overflow-y:auto;">
|
|
1064
|
+
<span style="color:var(--text-muted);font-size:0.8rem">Loading…</span>
|
|
1065
|
+
</div>
|
|
1066
|
+
</div>
|
|
1067
|
+
|
|
1068
|
+
<div class="setting-section">Agent Identity</div>
|
|
1069
|
+
|
|
1070
|
+
<div class="setting-row">
|
|
1071
|
+
<div>
|
|
1072
|
+
<div class="setting-label">Default Role</div>
|
|
1073
|
+
<div class="setting-desc">Used when no role is passed to memory/Hivemind tools</div>
|
|
1074
|
+
</div>
|
|
1075
|
+
<select class="setting-select" id="select-default-role" onchange="saveSetting('default_role', this.value)">
|
|
1076
|
+
<option value="global">global (shared)</option>
|
|
1077
|
+
<option value="dev">dev</option>
|
|
1078
|
+
<option value="qa">qa</option>
|
|
1079
|
+
<option value="pm">pm</option>
|
|
1080
|
+
<option value="lead">lead</option>
|
|
1081
|
+
<option value="security">security</option>
|
|
1082
|
+
<option value="ux">ux</option>
|
|
1083
|
+
</select>
|
|
1084
|
+
</div>
|
|
1085
|
+
|
|
1086
|
+
<div class="setting-row">
|
|
1087
|
+
<div>
|
|
1088
|
+
<div class="setting-label">Agent Name</div>
|
|
1089
|
+
<div class="setting-desc">Display name shown in Hivemind Radar (e.g. Dmitri, Dev Alex)</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
<input type="text" id="input-agent-name"
|
|
1092
|
+
placeholder="e.g. Dmitri"
|
|
1093
|
+
style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 130px;"
|
|
1094
|
+
onchange="saveSetting('agent_name', this.value)"
|
|
1095
|
+
oninput="clearTimeout(this._t); this._t=setTimeout(()=>saveSetting('agent_name',this.value),800)" />
|
|
1096
|
+
</div>
|
|
1097
|
+
|
|
1098
|
+
<span class="setting-saved" id="savedToast">Saved ✓</span>
|
|
1099
|
+
</div><!-- /spanel-settings -->
|
|
1100
|
+
|
|
1101
|
+
<!-- Skills panel -->
|
|
1102
|
+
<div class="s-tab-panel" id="spanel-skills">
|
|
1103
|
+
<div class="skill-role-row">
|
|
1104
|
+
<label>Role</label>
|
|
1105
|
+
<select class="skill-role-select" id="skillRoleSelect" onchange="loadSkillForRole(this.value)">
|
|
1106
|
+
<option value="global">🌐 global</option>
|
|
1107
|
+
<option value="dev">🛠️ dev</option>
|
|
1108
|
+
<option value="qa">🔍 qa</option>
|
|
1109
|
+
<option value="pm">📋 pm</option>
|
|
1110
|
+
<option value="lead">🏗️ lead</option>
|
|
1111
|
+
<option value="security">🔒 security</option>
|
|
1112
|
+
<option value="ux">🎨 ux</option>
|
|
1113
|
+
</select>
|
|
1114
|
+
</div>
|
|
1115
|
+
<textarea class="skill-textarea" id="skillTextarea"
|
|
1116
|
+
placeholder="Paste rules, conventions, or prompts for this role...
|
|
1117
|
+
Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode\n- Log errors to console.error"
|
|
1118
|
+
oninput="document.getElementById('skillCharCount').textContent = this.value.length + ' chars'">
|
|
1119
|
+
</textarea>
|
|
1120
|
+
<div class="skill-char-count" id="skillCharCount">0 chars</div>
|
|
1121
|
+
<div class="skill-actions">
|
|
1122
|
+
<button class="skill-save-btn" onclick="saveCurrentSkill()">💾 Save</button>
|
|
1123
|
+
<label class="skill-upload-btn" title="Upload a .md or .txt file">
|
|
1124
|
+
📎 Upload file
|
|
1125
|
+
<input type="file" accept=".md,.txt,.markdown" style="display:none"
|
|
1126
|
+
onchange="handleSkillUpload(this)">
|
|
1127
|
+
</label>
|
|
1128
|
+
<button class="skill-clear-btn" onclick="clearCurrentSkill()">🗑️ Clear</button>
|
|
1129
|
+
</div>
|
|
1130
|
+
<div class="skill-hint">
|
|
1131
|
+
Skills are auto-injected into <code>session_load_context</code> responses for this role.<br>
|
|
1132
|
+
Use Markdown. Changes take effect immediately — no restart needed.
|
|
1133
|
+
</div>
|
|
1134
|
+
</div><!-- /spanel-skills -->
|
|
1135
|
+
|
|
1136
|
+
<!-- AI Providers panel (v4.4) -->
|
|
1137
|
+
<div class="s-tab-panel" id="spanel-providers">
|
|
1138
|
+
|
|
1139
|
+
<div class="setting-section">Text Provider <span class="boot-badge">Restart Required</span></div>
|
|
1140
|
+
|
|
1141
|
+
<!-- ── Text Provider ──────────────────────────────── -->
|
|
1142
|
+
<div class="setting-row">
|
|
1143
|
+
<div>
|
|
1144
|
+
<div class="setting-label">Text Provider</div>
|
|
1145
|
+
<div class="setting-desc">LLM used for compaction, briefing, security scan & fact merging</div>
|
|
1146
|
+
</div>
|
|
1147
|
+
<select id="select-text-provider"
|
|
1148
|
+
style="padding: 0.2rem 0.4rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); cursor: pointer;"
|
|
1149
|
+
onchange="onTextProviderChange(this.value)">
|
|
1150
|
+
<option value="gemini">🔵 Gemini (Google)</option>
|
|
1151
|
+
<option value="openai">🟢 OpenAI / Ollama</option>
|
|
1152
|
+
<option value="anthropic">🟣 Anthropic (Claude)</option>
|
|
1153
|
+
</select>
|
|
1154
|
+
</div>
|
|
1155
|
+
|
|
1156
|
+
<!-- Gemini text fields -->
|
|
1157
|
+
<div id="provider-fields-gemini">
|
|
1158
|
+
<div class="setting-row">
|
|
1159
|
+
<div>
|
|
1160
|
+
<div class="setting-label">Google API Key</div>
|
|
1161
|
+
<div class="setting-desc">GOOGLE_API_KEY — required for Gemini text & embeddings</div>
|
|
1162
|
+
</div>
|
|
1163
|
+
<input type="password" id="input-google-api-key"
|
|
1164
|
+
placeholder="AIza…"
|
|
1165
|
+
style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 180px;"
|
|
1166
|
+
onchange="saveBootSetting('GOOGLE_API_KEY', this.value)"
|
|
1167
|
+
oninput="clearTimeout(this._pt); this._pt=setTimeout(()=>saveBootSetting('GOOGLE_API_KEY',this.value),800)" />
|
|
1168
|
+
</div>
|
|
1169
|
+
</div>
|
|
1170
|
+
|
|
1171
|
+
<!-- OpenAI / Ollama text fields -->
|
|
1172
|
+
<div id="provider-fields-openai" style="display:none">
|
|
1173
|
+
<div class="setting-row">
|
|
1174
|
+
<div>
|
|
1175
|
+
<div class="setting-label">API Key</div>
|
|
1176
|
+
<div class="setting-desc">Leave blank for Ollama / LM Studio (local endpoints)</div>
|
|
1177
|
+
</div>
|
|
1178
|
+
<input type="password" id="input-openai-api-key"
|
|
1179
|
+
placeholder="sk-… (blank for Ollama)"
|
|
1180
|
+
style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 180px;"
|
|
1181
|
+
onchange="saveBootSetting('openai_api_key', this.value)"
|
|
1182
|
+
oninput="clearTimeout(this._pt); this._pt=setTimeout(()=>saveBootSetting('openai_api_key',this.value),800)" />
|
|
1183
|
+
</div>
|
|
1184
|
+
<div class="setting-row">
|
|
1185
|
+
<div>
|
|
1186
|
+
<div class="setting-label">Base URL</div>
|
|
1187
|
+
<div class="setting-desc">Ollama: http://localhost:11434/v1 · LM Studio: http://localhost:1234/v1</div>
|
|
1188
|
+
</div>
|
|
1189
|
+
<input type="text" id="input-openai-base-url"
|
|
1190
|
+
placeholder="https://api.openai.com/v1"
|
|
1191
|
+
style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 220px;"
|
|
1192
|
+
onchange="saveBootSetting('openai_base_url', this.value)"
|
|
1193
|
+
oninput="clearTimeout(this._pu); this._pu=setTimeout(()=>saveBootSetting('openai_base_url',this.value),800)" />
|
|
1194
|
+
</div>
|
|
1195
|
+
<div class="setting-row">
|
|
1196
|
+
<div>
|
|
1197
|
+
<div class="setting-label">Chat Model</div>
|
|
1198
|
+
<div class="setting-desc">Used for compaction, briefing, security scan</div>
|
|
1199
|
+
</div>
|
|
1200
|
+
<input type="text" id="input-openai-model"
|
|
1201
|
+
placeholder="gpt-4o-mini"
|
|
1202
|
+
style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 160px;"
|
|
1203
|
+
onchange="saveBootSetting('openai_model', this.value)"
|
|
1204
|
+
oninput="clearTimeout(this._pm); this._pm=setTimeout(()=>saveBootSetting('openai_model',this.value),800)" />
|
|
1205
|
+
</div>
|
|
1206
|
+
</div>
|
|
1207
|
+
|
|
1208
|
+
<!-- Anthropic / Claude text fields -->
|
|
1209
|
+
<div id="provider-fields-anthropic" style="display:none">
|
|
1210
|
+
<div class="setting-row">
|
|
1211
|
+
<div>
|
|
1212
|
+
<div class="setting-label">Anthropic API Key</div>
|
|
1213
|
+
<div class="setting-desc">Required. Get yours at console.anthropic.com</div>
|
|
1214
|
+
</div>
|
|
1215
|
+
<input type="password" id="input-anthropic-api-key"
|
|
1216
|
+
placeholder="sk-ant-…"
|
|
1217
|
+
style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 200px;"
|
|
1218
|
+
onchange="saveBootSetting('anthropic_api_key', this.value)"
|
|
1219
|
+
oninput="clearTimeout(this._pa); this._pa=setTimeout(()=>saveBootSetting('anthropic_api_key',this.value),800)" />
|
|
1220
|
+
</div>
|
|
1221
|
+
<div class="setting-row">
|
|
1222
|
+
<div>
|
|
1223
|
+
<div class="setting-label">Claude Model</div>
|
|
1224
|
+
<div class="setting-desc">claude-3-5-sonnet for quality · claude-3-haiku for speed & cost</div>
|
|
1225
|
+
</div>
|
|
1226
|
+
<input type="text" id="input-anthropic-model"
|
|
1227
|
+
placeholder="claude-3-5-sonnet-20241022"
|
|
1228
|
+
style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 220px;"
|
|
1229
|
+
onchange="saveBootSetting('anthropic_model', this.value)"
|
|
1230
|
+
oninput="clearTimeout(this._pam); this._pam=setTimeout(()=>saveBootSetting('anthropic_model',this.value),800)" />
|
|
1231
|
+
</div>
|
|
1232
|
+
</div>
|
|
1233
|
+
|
|
1234
|
+
<!-- ── Embedding Provider (always visible) ─────────── -->
|
|
1235
|
+
<div class="setting-section" style="margin-top:1.2rem">Embedding Provider <span class="boot-badge">Restart Required</span></div>
|
|
1236
|
+
|
|
1237
|
+
<div class="setting-row">
|
|
1238
|
+
<div>
|
|
1239
|
+
<div class="setting-label">Embedding Provider</div>
|
|
1240
|
+
<div class="setting-desc">Source for vector embeddings used by semantic memory search</div>
|
|
1241
|
+
</div>
|
|
1242
|
+
<select id="select-embedding-provider"
|
|
1243
|
+
style="padding: 0.2rem 0.4rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); cursor: pointer;"
|
|
1244
|
+
onchange="onEmbeddingProviderChange(this.value)">
|
|
1245
|
+
<option value="auto">🔄 Auto (same as Text Provider)</option>
|
|
1246
|
+
<option value="gemini">🔵 Gemini</option>
|
|
1247
|
+
<option value="openai">🟢 OpenAI / Ollama</option>
|
|
1248
|
+
</select>
|
|
1249
|
+
</div>
|
|
1250
|
+
|
|
1251
|
+
<!-- Anthropic + auto warning: shown when text=anthropic AND embed=auto -->
|
|
1252
|
+
<div id="anthropic-embed-warning" style="display:none;margin-top:0.5rem;padding:0.5rem 0.75rem;background:rgba(251,146,60,0.1);border:1px solid rgba(251,146,60,0.3);border-radius:6px;font-size:0.78rem;color:#fb923c;line-height:1.5">
|
|
1253
|
+
⚠️ <strong>Anthropic has no native embedding API.</strong>
|
|
1254
|
+
Auto mode will route embeddings to <strong>Gemini</strong>.
|
|
1255
|
+
Set Embedding Provider to <strong>OpenAI / Ollama</strong> to use a local model (e.g. <code>nomic-embed-text</code>).
|
|
1256
|
+
</div>
|
|
1257
|
+
|
|
1258
|
+
<!-- OpenAI embedding model field (shown when embedding_provider = openai) -->
|
|
1259
|
+
<div id="embed-fields-openai" style="display:none">
|
|
1260
|
+
<div class="setting-row">
|
|
1261
|
+
<div>
|
|
1262
|
+
<div class="setting-label">Embedding Model</div>
|
|
1263
|
+
<div class="setting-desc">Must output 768 dims. Ollama: nomic-embed-text · OpenAI: text-embedding-3-small</div>
|
|
1264
|
+
</div>
|
|
1265
|
+
<input type="text" id="input-openai-embedding-model"
|
|
1266
|
+
placeholder="text-embedding-3-small"
|
|
1267
|
+
style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 210px;"
|
|
1268
|
+
onchange="saveBootSetting('openai_embedding_model', this.value)"
|
|
1269
|
+
oninput="clearTimeout(this._pe); this._pe=setTimeout(()=>saveBootSetting('openai_embedding_model',this.value),800)" />
|
|
1270
|
+
</div>
|
|
1271
|
+
</div>
|
|
1272
|
+
|
|
1273
|
+
<div style="margin-top:1rem;padding:0.6rem 0.8rem;background:rgba(139,92,246,0.08);border:1px solid rgba(139,92,246,0.2);border-radius:6px;font-size:0.78rem;color:var(--text-secondary);line-height:1.5">
|
|
1274
|
+
💡 <strong>Cost-optimized setup:</strong> Text Provider → <code>Anthropic</code>, Embedding Provider → <code>OpenAI / Ollama</code>.<br>
|
|
1275
|
+
Use Claude 3.5 Sonnet for reasoning & <code>nomic-embed-text</code> (free, local) for embeddings.
|
|
1276
|
+
</div>
|
|
1277
|
+
|
|
1278
|
+
<span class="setting-saved" id="savedToastProviders">Saved ✓</span>
|
|
1279
|
+
</div><!-- /spanel-providers -->
|
|
1280
|
+
|
|
1281
|
+
<!-- ─── Observability panel (v4.6.0 — OTel) ───────────────────────── -->
|
|
1282
|
+
<div class="s-tab-panel" id="spanel-observability">
|
|
1283
|
+
|
|
1284
|
+
<div class="setting-section">OpenTelemetry (OTel)</div>
|
|
1285
|
+
|
|
1286
|
+
<div class="setting-row" style="align-items: flex-start; margin-bottom: 1rem;">
|
|
1287
|
+
<div class="setting-desc" style="margin: 0;">
|
|
1288
|
+
Export distributed traces to
|
|
1289
|
+
<a href="https://www.jaegertracing.io" target="_blank" rel="noopener" style="color: var(--accent);">Jaeger</a>,
|
|
1290
|
+
<a href="https://grafana.com/oss/tempo/" target="_blank" rel="noopener" style="color: var(--accent);">Grafana Tempo</a>, or
|
|
1291
|
+
<a href="https://zipkin.io" target="_blank" rel="noopener" style="color: var(--accent);">Zipkin</a>.
|
|
1292
|
+
Provides a full latency waterfall for every MCP tool call, LLM provider hop, and background worker task.
|
|
1293
|
+
<br><br>
|
|
1294
|
+
<code style="font-size: 0.8rem; background: var(--bg-hover); padding: 2px 6px; border-radius: 4px;">
|
|
1295
|
+
docker run -d -p 4318:4318 -p 16686:16686 jaegertracing/all-in-one
|
|
1296
|
+
</code>
|
|
1297
|
+
→ open <a href="http://localhost:16686" target="_blank" rel="noopener" style="color: var(--accent);">localhost:16686</a>
|
|
1298
|
+
</div>
|
|
1299
|
+
</div>
|
|
1300
|
+
|
|
1301
|
+
<!-- Enable toggle -->
|
|
1302
|
+
<div class="setting-row">
|
|
1303
|
+
<div>
|
|
1304
|
+
<div class="setting-label">Enable OpenTelemetry</div>
|
|
1305
|
+
<div class="setting-desc">Activates the W3C tracing pipeline. <strong>Requires server restart.</strong></div>
|
|
1306
|
+
</div>
|
|
1307
|
+
<label class="toggle-switch">
|
|
1308
|
+
<input type="checkbox" id="input-otel-enabled"
|
|
1309
|
+
onchange="saveBootSetting('otel_enabled', this.checked ? 'true' : 'false')">
|
|
1310
|
+
<span class="slider"></span>
|
|
1311
|
+
</label>
|
|
1312
|
+
</div>
|
|
1313
|
+
|
|
1314
|
+
<!-- OTLP endpoint -->
|
|
1315
|
+
<div class="setting-row">
|
|
1316
|
+
<div style="flex: 0 0 auto; min-width: 160px;">
|
|
1317
|
+
<div class="setting-label">OTLP HTTP Endpoint</div>
|
|
1318
|
+
<div class="setting-desc">Where spans are exported.</div>
|
|
1319
|
+
</div>
|
|
1320
|
+
<input type="text" id="input-otel-endpoint"
|
|
1321
|
+
class="setting-input"
|
|
1322
|
+
placeholder="http://localhost:4318/v1/traces"
|
|
1323
|
+
style="flex: 1;"
|
|
1324
|
+
onchange="saveBootSetting('otel_endpoint', this.value)"
|
|
1325
|
+
oninput="clearTimeout(this._pt); this._pt=setTimeout(()=>saveBootSetting('otel_endpoint',this.value),800)" />
|
|
1326
|
+
</div>
|
|
1327
|
+
|
|
1328
|
+
<!-- Service name -->
|
|
1329
|
+
<div class="setting-row">
|
|
1330
|
+
<div style="flex: 0 0 auto; min-width: 160px;">
|
|
1331
|
+
<div class="setting-label">Service Name</div>
|
|
1332
|
+
<div class="setting-desc">Label shown in the trace UI.</div>
|
|
1333
|
+
</div>
|
|
1334
|
+
<input type="text" id="input-otel-service"
|
|
1335
|
+
class="setting-input"
|
|
1336
|
+
placeholder="prism-mcp-server"
|
|
1337
|
+
style="flex: 1;"
|
|
1338
|
+
onchange="saveBootSetting('otel_service_name', this.value)"
|
|
1339
|
+
oninput="clearTimeout(this._ps); this._ps=setTimeout(()=>saveBootSetting('otel_service_name',this.value),800)" />
|
|
1340
|
+
</div>
|
|
1341
|
+
|
|
1342
|
+
<!-- Expected trace waterfall diagram -->
|
|
1343
|
+
<div class="setting-row" style="flex-direction: column; align-items: flex-start; margin-top: 0.5rem;">
|
|
1344
|
+
<div class="setting-label" style="margin-bottom: 0.5rem;">Expected Trace Waterfall</div>
|
|
1345
|
+
<pre style="font-size: 0.78rem; background: var(--bg-hover); padding: 0.8rem 1rem; border-radius: 6px; color: var(--text-secondary); line-height: 1.6; width: 100%; box-sizing: border-box; overflow-x: auto;">mcp.call_tool [e.g. session_save_image, ~50 ms]
|
|
1346
|
+
└─ worker.vlm_caption [~2–5 s, outlives parent ✓]
|
|
1347
|
+
└─ llm.generate_image_description [~1–4 s]
|
|
1348
|
+
└─ llm.generate_embedding [~200 ms]</pre>
|
|
1349
|
+
</div>
|
|
1350
|
+
|
|
1351
|
+
<span class="setting-saved" id="savedToastOtel">Saved ✓</span>
|
|
1352
|
+
</div><!-- /spanel-observability -->
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
</div>
|
|
1356
|
+
</div>
|
|
1357
|
+
</div>
|
|
1358
|
+
|
|
1359
|
+
<!-- Fixed toast for cleanup feedback -->
|
|
1360
|
+
<div class="toast-fixed" id="fixedToast"></div>
|
|
1361
|
+
|
|
1362
|
+
<script>
|
|
1363
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1364
|
+
// COMPATIBILITY RULE: This entire <script> block MUST use ES5 only.
|
|
1365
|
+
// - Use 'var' (NEVER 'const' or 'let')
|
|
1366
|
+
// - Use 'function(){}' (NEVER '=>' arrow functions)
|
|
1367
|
+
// - NO optional chaining '?.'
|
|
1368
|
+
// - NO template literals (backticks) — use string concatenation
|
|
1369
|
+
// - NO destructuring, spread, or other ES6+ syntax
|
|
1370
|
+
// This HTML is served as a raw template literal; mixing ES6 in the
|
|
1371
|
+
// inline script causes SyntaxError in some browser/context combos.
|
|
1372
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1373
|
+
|
|
1374
|
+
// ─── TABS & SEARCH (v6.0) ───
|
|
1375
|
+
function switchMainTab(tabId) {
|
|
1376
|
+
document.getElementById('mtab-project').classList.toggle('active', tabId === 'project');
|
|
1377
|
+
document.getElementById('mtab-search').classList.toggle('active', tabId === 'search');
|
|
1378
|
+
document.getElementById('mtab-factory').classList.toggle('active', tabId === 'factory');
|
|
1379
|
+
|
|
1380
|
+
document.getElementById('content').style.display = tabId === 'project' ? '' : 'none';
|
|
1381
|
+
document.getElementById('search-content').style.display = tabId === 'search' ? 'block' : 'none';
|
|
1382
|
+
document.getElementById('factory-content').style.display = tabId === 'factory' ? 'block' : 'none';
|
|
1383
|
+
|
|
1384
|
+
if (tabId === 'search') {
|
|
1385
|
+
document.getElementById('searchInput').focus();
|
|
1386
|
+
}
|
|
1387
|
+
if (tabId === 'factory') {
|
|
1388
|
+
loadPipelines();
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// ─── DARK FACTORY (v7.3) ───
|
|
1393
|
+
var factoryPollTimer = null;
|
|
1394
|
+
|
|
1395
|
+
function loadPipelines() {
|
|
1396
|
+
var statusFilter = document.getElementById('factoryStatusFilter').value;
|
|
1397
|
+
var url = '/api/pipelines';
|
|
1398
|
+
if (statusFilter) url += '?status=' + encodeURIComponent(statusFilter);
|
|
1399
|
+
|
|
1400
|
+
fetch(url)
|
|
1401
|
+
.then(function(r) { return r.json(); })
|
|
1402
|
+
.then(function(data) {
|
|
1403
|
+
var list = document.getElementById('factoryList');
|
|
1404
|
+
var count = document.getElementById('factoryCount');
|
|
1405
|
+
var pipelines = data.pipelines || [];
|
|
1406
|
+
count.textContent = pipelines.length + ' pipeline' + (pipelines.length !== 1 ? 's' : '');
|
|
1407
|
+
|
|
1408
|
+
if (pipelines.length === 0) {
|
|
1409
|
+
list.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-muted)"><div style="font-size:2rem;margin-bottom:0.5rem">🏭</div>No pipelines found. Use <code>session_start_pipeline</code> to create one.</div>';
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
var html = '<div style="display:flex;flex-direction:column;gap:0.5rem">';
|
|
1414
|
+
for (var i = 0; i < pipelines.length; i++) {
|
|
1415
|
+
var p = pipelines[i];
|
|
1416
|
+
var emoji = p.status === 'COMPLETED' ? '✅' : p.status === 'FAILED' ? '❌' : p.status === 'ABORTED' ? '🛑' : p.status === 'RUNNING' ? '⏳' : p.status === 'PENDING' ? '⏸' : '📋';
|
|
1417
|
+
var statusColor = p.status === 'COMPLETED' ? 'var(--accent-green)' : p.status === 'FAILED' ? 'var(--accent-rose)' : p.status === 'ABORTED' ? 'var(--accent-amber)' : p.status === 'RUNNING' ? 'var(--accent-purple)' : p.status === 'PENDING' ? 'var(--accent-blue, #3b82f6)' : 'var(--text-muted)';
|
|
1418
|
+
var isActive = p.status === 'RUNNING' || p.status === 'PENDING';
|
|
1419
|
+
var objective = (p.parsedSpec && p.parsedSpec.objective) ? p.parsedSpec.objective : '(unknown)';
|
|
1420
|
+
if (objective.length > 120) objective = objective.slice(0, 120) + '…';
|
|
1421
|
+
var maxIter = (p.parsedSpec && p.parsedSpec.maxIterations) ? p.parsedSpec.maxIterations : '?';
|
|
1422
|
+
|
|
1423
|
+
html += '<div style="padding:0.75rem 1rem;background:rgba(15,23,42,0.6);border-radius:8px;border-left:3px solid ' + statusColor + ';">';
|
|
1424
|
+
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">';
|
|
1425
|
+
html += '<span style="font-weight:600;color:var(--text-primary)">' + emoji + ' ' + p.status + '</span>';
|
|
1426
|
+
html += '<span style="font-size:0.7rem;font-family:var(--font-mono);color:var(--text-muted)">' + p.id.slice(0, 8) + '…</span>';
|
|
1427
|
+
html += '</div>';
|
|
1428
|
+
html += '<div style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.35rem">' + objective + '</div>';
|
|
1429
|
+
html += '<div style="display:flex;gap:1rem;font-size:0.72rem;color:var(--text-muted);flex-wrap:wrap">';
|
|
1430
|
+
html += '<span>📁 ' + (p.project || '?') + '</span>';
|
|
1431
|
+
html += '<span>🔄 ' + p.iteration + ' / ' + maxIter + '</span>';
|
|
1432
|
+
html += '<span>📍 ' + (p.current_step || '?') + '</span>';
|
|
1433
|
+
html += '<span>🕐 ' + new Date(p.updated_at).toLocaleString() + '</span>';
|
|
1434
|
+
html += '</div>';
|
|
1435
|
+
if (p.error) {
|
|
1436
|
+
html += '<div style="font-size:0.72rem;color:var(--accent-rose);margin-top:0.35rem;padding:0.3rem 0.5rem;background:rgba(244,63,94,0.08);border-radius:4px">⚠ ' + p.error.slice(0, 200) + '</div>';
|
|
1437
|
+
}
|
|
1438
|
+
if (isActive) {
|
|
1439
|
+
html += '<div style="margin-top:0.5rem"><button onclick="abortPipeline(\'' + p.id + '\')" class="cleanup-btn" style="font-size:0.72rem">🛑 Abort Pipeline</button></div>';
|
|
1440
|
+
}
|
|
1441
|
+
html += '</div>';
|
|
1442
|
+
}
|
|
1443
|
+
html += '</div>';
|
|
1444
|
+
list.innerHTML = html;
|
|
1445
|
+
|
|
1446
|
+
// Auto-poll if any pipeline is running
|
|
1447
|
+
var hasActive = pipelines.some(function(p) { return p.status === 'RUNNING' || p.status === 'PENDING'; });
|
|
1448
|
+
clearInterval(factoryPollTimer);
|
|
1449
|
+
if (hasActive) {
|
|
1450
|
+
factoryPollTimer = setInterval(function() {
|
|
1451
|
+
if (document.getElementById('factory-content').style.display !== 'none') loadPipelines();
|
|
1452
|
+
else clearInterval(factoryPollTimer);
|
|
1453
|
+
}, 10000);
|
|
1454
|
+
}
|
|
1455
|
+
})
|
|
1456
|
+
.catch(function(err) {
|
|
1457
|
+
document.getElementById('factoryList').innerHTML = '<div style="color:var(--accent-rose);padding:1rem">Failed to load pipelines: ' + err.message + '</div>';
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function abortPipeline(id) {
|
|
1462
|
+
if (!confirm('Abort pipeline ' + id.slice(0, 8) + '…?')) return;
|
|
1463
|
+
fetch('/api/pipelines/' + id + '/abort', { method: 'POST' })
|
|
1464
|
+
.then(function(r) { return r.json(); })
|
|
1465
|
+
.then(function(data) {
|
|
1466
|
+
if (data.ok) {
|
|
1467
|
+
showToast('Pipeline aborted');
|
|
1468
|
+
loadPipelines();
|
|
1469
|
+
} else {
|
|
1470
|
+
showToast('Failed: ' + (data.error || 'Unknown error'));
|
|
1471
|
+
}
|
|
1472
|
+
})
|
|
1473
|
+
.catch(function(err) { showToast('Abort failed: ' + err.message); });
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
var searchTimeout = null;
|
|
1477
|
+
var searchAbortController = null;
|
|
1478
|
+
|
|
1479
|
+
function performSearch() {
|
|
1480
|
+
var input = document.getElementById('searchInput');
|
|
1481
|
+
var boost = document.getElementById('searchContextBoost');
|
|
1482
|
+
var resultsDiv = document.getElementById('searchResults');
|
|
1483
|
+
var query = input.value.trim();
|
|
1484
|
+
|
|
1485
|
+
if (!query) {
|
|
1486
|
+
if (searchAbortController) searchAbortController.abort();
|
|
1487
|
+
resultsDiv.innerHTML = '<div style="color:var(--text-muted); font-size:0.9rem; padding: 2rem; text-align:center;">Enter a query to search the neural ledger via embeddings...</div>';
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
resultsDiv.innerHTML = '<div class="loading" style="padding:2rem;"><span class="spinner"></span> Searching neural memory via embeddings...</div>';
|
|
1492
|
+
|
|
1493
|
+
var project = document.getElementById('projectSelect').value;
|
|
1494
|
+
var url = '/api/search?q=' + encodeURIComponent(query);
|
|
1495
|
+
if (project) url += '&project=' + encodeURIComponent(project);
|
|
1496
|
+
if (boost.checked) url += '&boost=true';
|
|
1497
|
+
|
|
1498
|
+
if (searchAbortController) searchAbortController.abort();
|
|
1499
|
+
searchAbortController = new AbortController();
|
|
1500
|
+
|
|
1501
|
+
fetch(url, { signal: searchAbortController.signal }).then(function(res) { return res.json(); }).then(function(data) {
|
|
1502
|
+
if (data.error) throw new Error(data.error);
|
|
1503
|
+
|
|
1504
|
+
if (!data.results || data.results.length === 0) {
|
|
1505
|
+
resultsDiv.innerHTML = '<div style="color:var(--text-muted); padding: 2rem; text-align:center;">No matching memories found for this query.</div>';
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// Extract searchable terms for highlighting (length > 2)
|
|
1510
|
+
var queryTerms = query.split(/\\s+/).filter(function(w) { return w.length > 2; });
|
|
1511
|
+
var termRegex = queryTerms.length > 0
|
|
1512
|
+
? new RegExp('(' + queryTerms.map(function(w) { return w.replace(/[.*+?^$()|[\\]\\\\{}]/g, '\\\\$&'); }).join('|') + ')', 'gi')
|
|
1513
|
+
: null;
|
|
1514
|
+
|
|
1515
|
+
function highlight(text) {
|
|
1516
|
+
var escaped = escapeHtml(text || '');
|
|
1517
|
+
if (termRegex) {
|
|
1518
|
+
escaped = escaped.replace(termRegex, '<mark style="background: rgba(168, 85, 247, 0.4); color: inherit; padding: 0 0.1rem; border-radius: 2px;">$1</mark>');
|
|
1519
|
+
}
|
|
1520
|
+
return escaped;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
resultsDiv.innerHTML = data.results.map(function(r) {
|
|
1524
|
+
var isGraduated = r.importance >= 7;
|
|
1525
|
+
var opacity = isGraduated ? 1 : 0.8;
|
|
1526
|
+
var borderStyle = isGraduated ? 'border-left: 3px solid var(--accent-purple); padding-left: 0.8rem;' : '';
|
|
1527
|
+
var decisionsHtml = '';
|
|
1528
|
+
if (r.decisions && r.decisions.length > 0) {
|
|
1529
|
+
decisionsHtml = '<ul class="tag-list" style="margin-top:0.75rem;">' +
|
|
1530
|
+
r.decisions.map(function(d) { return '<li class="tag">💡 ' + highlight(d) + '</li>'; }).join('') +
|
|
1531
|
+
'</ul>';
|
|
1532
|
+
}
|
|
1533
|
+
return '<div class="entry" style="opacity: ' + opacity + '; ' + borderStyle + '">' +
|
|
1534
|
+
'<div class="entry-meta" style="justify-content:space-between; margin-bottom:0.5rem;">' +
|
|
1535
|
+
'<span>📁 ' + escapeHtml(r.project) + ' • 🕒 ' + new Date(r.session_date || r.created_at || Date.now()).toLocaleDateString() + '</span>' +
|
|
1536
|
+
'<div style="display:flex; gap:0.5rem; font-size:0.75rem;">' +
|
|
1537
|
+
'<span class="badge" title="Similarity Score (Semantic Match)" style="background:rgba(6,182,212,0.1); color:var(--accent-cyan); border:1px solid rgba(6,182,212,0.3);">' +
|
|
1538
|
+
'🎯 ' + (r.similarity * 100).toFixed(1) + '%' +
|
|
1539
|
+
'</span>' +
|
|
1540
|
+
'<span class="badge badge-purple" title="Ebbinghaus Importance (Recency/Reinforcement)">' +
|
|
1541
|
+
'⭐ ' + (r.importance || 0).toFixed(1) +
|
|
1542
|
+
'</span>' +
|
|
1543
|
+
'</div>' +
|
|
1544
|
+
'</div>' +
|
|
1545
|
+
'<div class="entry-summary" style="font-size:0.9rem; line-height: 1.5;">' + highlight(r.summary) + '</div>' +
|
|
1546
|
+
decisionsHtml +
|
|
1547
|
+
'</div>';
|
|
1548
|
+
}).join('');
|
|
1549
|
+
}).catch(function(err) {
|
|
1550
|
+
if (err.name === 'AbortError') return; // Ignore aborted fetches
|
|
1551
|
+
resultsDiv.innerHTML = '<div style="padding:1rem; color:var(--accent-rose);">❌ Failed to search memory: ' + escapeHtml(err.message) + '</div>';
|
|
1552
|
+
})
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
var _searchInput = document.getElementById('searchInput');
|
|
1556
|
+
if (_searchInput) _searchInput.addEventListener('input', function() {
|
|
1557
|
+
clearTimeout(searchTimeout);
|
|
1558
|
+
searchTimeout = setTimeout(performSearch, 300);
|
|
1559
|
+
});
|
|
1560
|
+
var _searchBoost = document.getElementById('searchContextBoost');
|
|
1561
|
+
if (_searchBoost) _searchBoost.addEventListener('change', performSearch);
|
|
1562
|
+
|
|
1563
|
+
|
|
1564
|
+
// Role icon map
|
|
1565
|
+
var ROLE_ICONS = {dev:'🛠️',qa:'🔍',pm:'📋',lead:'🏗️',security:'🔒',ux:'🎨',global:'🌐',cmo:'📢'};
|
|
1566
|
+
|
|
1567
|
+
// Load and render the identity chip from settings
|
|
1568
|
+
function loadIdentityChip() {
|
|
1569
|
+
fetch('/api/settings').then(function(res) { return res.json(); }).then(function(data) {
|
|
1570
|
+
var s = data.settings || {};
|
|
1571
|
+
var role = s.default_role || '';
|
|
1572
|
+
var name = s.agent_name || '';
|
|
1573
|
+
var chip = document.getElementById('identityChip');
|
|
1574
|
+
if (!chip) return;
|
|
1575
|
+
if (role && role !== 'global' || name) {
|
|
1576
|
+
var icon = ROLE_ICONS[role] || '🤖';
|
|
1577
|
+
var label = name ? (role && role !== 'global' ? role + ' · ' + name : name) : role;
|
|
1578
|
+
chip.innerHTML = '<span class="role-icon">' + icon + '</span><span class="identity-label">' + escapeHtml(label) + '</span>';
|
|
1579
|
+
chip.style.display = 'flex';
|
|
1580
|
+
} else {
|
|
1581
|
+
chip.style.display = 'none';
|
|
1582
|
+
}
|
|
1583
|
+
} catch(e) { /* silently skip */ }
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Auto-load project list on page load
|
|
1587
|
+
(function() {
|
|
1588
|
+
fetch('/api/projects').then(function(res) { return res.json(); }).then(function(data) {
|
|
1589
|
+
var select = document.getElementById('projectSelect');
|
|
1590
|
+
if (data.projects && data.projects.length > 0) {
|
|
1591
|
+
select.innerHTML = '<option value="">— Select a project —</option>' +
|
|
1592
|
+
data.projects.map(function(p) { return '<option value="' + p + '">' + p + '</option>'; }).join('');
|
|
1593
|
+
|
|
1594
|
+
var gp = document.getElementById('graphProjectFilter');
|
|
1595
|
+
if (gp) {
|
|
1596
|
+
gp.innerHTML = '<option value="">All Projects</option>' +
|
|
1597
|
+
data.projects.map(function(p) { return '<option value="' + p + '">' + p + '</option>'; }).join('');
|
|
1598
|
+
}
|
|
1599
|
+
} else {
|
|
1600
|
+
select.innerHTML = '<option value="">No projects found</option>';
|
|
1601
|
+
}
|
|
1602
|
+
}).catch(function(e) {
|
|
1603
|
+
document.getElementById('projectSelect').innerHTML = '<option value="">Error loading projects</option>';
|
|
1604
|
+
})
|
|
1605
|
+
// Load identity chip once settings are available
|
|
1606
|
+
loadIdentityChip();
|
|
1607
|
+
})();
|
|
1608
|
+
|
|
1609
|
+
function loadProject() {
|
|
1610
|
+
var project = document.getElementById('projectSelect').value;
|
|
1611
|
+
if (!project) return;
|
|
1612
|
+
|
|
1613
|
+
document.getElementById('welcome').style.display = 'none';
|
|
1614
|
+
document.getElementById('content').style.display = 'none';
|
|
1615
|
+
document.getElementById('loading').style.display = 'block';
|
|
1616
|
+
|
|
1617
|
+
fetch('/api/project?name=' + encodeURIComponent(project));
|
|
1618
|
+
var data = await res.json();
|
|
1619
|
+
|
|
1620
|
+
// ─── Populate Context ───
|
|
1621
|
+
var ctx = data.context || {};
|
|
1622
|
+
document.getElementById('versionBadge').textContent = 'v' + (ctx.version || '?');
|
|
1623
|
+
document.getElementById('summary').textContent = ctx.last_summary || ctx.summary || 'No summary available.';
|
|
1624
|
+
|
|
1625
|
+
var todos = ctx.pending_todo || ctx.active_context || [];
|
|
1626
|
+
var todoList = document.getElementById('todos');
|
|
1627
|
+
if (Array.isArray(todos) && todos.length > 0) {
|
|
1628
|
+
todoList.innerHTML = todos.map(function(t) { return '<li>' + escapeHtml(t) + '</li>'; }).join('');
|
|
1629
|
+
} else {
|
|
1630
|
+
todoList.innerHTML = '<li style="color:var(--text-muted)">No pending TODOs</li>';
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// ─── Git ───
|
|
1634
|
+
var meta = ctx.metadata || {};
|
|
1635
|
+
document.getElementById('gitBranch').textContent = meta.git_branch || ctx.active_branch || '—';
|
|
1636
|
+
document.getElementById('gitSha').textContent = meta.last_commit_sha ? meta.last_commit_sha.substring(0, 12) : '—';
|
|
1637
|
+
document.getElementById('keyContext').textContent = ctx.key_context || '—';
|
|
1638
|
+
|
|
1639
|
+
// ─── Morning Briefing ───
|
|
1640
|
+
var briefingCard = document.getElementById('briefingCard');
|
|
1641
|
+
if (meta.morning_briefing) {
|
|
1642
|
+
document.getElementById('briefingText').textContent = meta.morning_briefing;
|
|
1643
|
+
briefingCard.style.display = 'block';
|
|
1644
|
+
} else {
|
|
1645
|
+
briefingCard.style.display = 'none';
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// ─── Visual Memory ───
|
|
1649
|
+
var visualCard = document.getElementById('visualCard');
|
|
1650
|
+
var visuals = meta.visual_memory || [];
|
|
1651
|
+
if (visuals.length > 0) {
|
|
1652
|
+
document.getElementById('visualList').innerHTML = visuals.map(function(v) {
|
|
1653
|
+
var dateStr = v.timestamp ? v.timestamp.split('T')[0] : '';
|
|
1654
|
+
return '<li><span class="visual-id">[' + escapeHtml(v.id) + ']</span> ' +
|
|
1655
|
+
escapeHtml(v.description) +
|
|
1656
|
+
'<span class="visual-date">' + dateStr + '</span></li>';
|
|
1657
|
+
}).join('');
|
|
1658
|
+
visualCard.style.display = 'block';
|
|
1659
|
+
} else {
|
|
1660
|
+
visualCard.style.display = 'none';
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// ─── History Timeline ───
|
|
1664
|
+
var historyEl = document.getElementById('historyTimeline');
|
|
1665
|
+
if (data.history && data.history.length > 0) {
|
|
1666
|
+
historyEl.innerHTML = data.history.map(function(h) {
|
|
1667
|
+
var snap = h.snapshot || {};
|
|
1668
|
+
var summary = snap.last_summary || snap.summary || 'Snapshot';
|
|
1669
|
+
return '<div class="timeline-item history">' +
|
|
1670
|
+
'<div class="meta"><span class="badge badge-purple">v' + h.version + '</span>' +
|
|
1671
|
+
'<span>' + formatDate(h.created_at) + '</span></div>' +
|
|
1672
|
+
escapeHtml(summary) + '</div>';
|
|
1673
|
+
}).join('');
|
|
1674
|
+
} else {
|
|
1675
|
+
historyEl.innerHTML = '<div style="color:var(--text-muted);font-size:0.85rem;padding:1rem;text-align:center">No time travel history yet.</div>';
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// ─── Ledger Timeline ───
|
|
1679
|
+
var ledgerEl = document.getElementById('ledgerTimeline');
|
|
1680
|
+
if (data.ledger && data.ledger.length > 0) {
|
|
1681
|
+
ledgerEl.innerHTML = data.ledger.map(function(l) {
|
|
1682
|
+
var summary = l.summary || l.content || 'Entry';
|
|
1683
|
+
var decisions = l.decisions;
|
|
1684
|
+
var extra = '';
|
|
1685
|
+
if (decisions && decisions.length > 0) {
|
|
1686
|
+
try {
|
|
1687
|
+
var parsed = typeof decisions === 'string' ? JSON.parse(decisions) : decisions;
|
|
1688
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
1689
|
+
extra = '<div style="margin-top:0.3rem;font-size:0.75rem;color:var(--accent-cyan)">Decisions: ' + parsed.join(', ') + '</div>';
|
|
1690
|
+
}
|
|
1691
|
+
} catch(e) {}
|
|
1692
|
+
}
|
|
1693
|
+
return '<div class="timeline-item">' +
|
|
1694
|
+
'<div class="meta"><span class="badge badge-amber">session</span>' +
|
|
1695
|
+
'<span>' + formatDate(l.created_at) + '</span></div>' +
|
|
1696
|
+
escapeHtml(summary) + extra + '</div>';
|
|
1697
|
+
}).join('');
|
|
1698
|
+
} else {
|
|
1699
|
+
ledgerEl.innerHTML = '<div style="color:var(--text-muted);font-size:0.85rem;padding:1rem;text-align:center">No ledger entries yet.</div>';
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// ─── Brain Health (v2.2.0) ───
|
|
1703
|
+
fetch('/api/health').then(function(healthRes) { return healthRes.json(); }).then(function(healthData) {
|
|
1704
|
+
var healthCard = document.getElementById('healthCard');
|
|
1705
|
+
var healthDot = document.getElementById('healthDot');
|
|
1706
|
+
var healthLabel = document.getElementById('healthLabel');
|
|
1707
|
+
var healthSummary = document.getElementById('healthSummary');
|
|
1708
|
+
var healthIssues = document.getElementById('healthIssues');
|
|
1709
|
+
|
|
1710
|
+
// Set the dot color based on status
|
|
1711
|
+
healthDot.className = 'health-dot ' + (healthData.status || 'unknown');
|
|
1712
|
+
|
|
1713
|
+
// Map status to emoji + label
|
|
1714
|
+
var statusMap = { healthy: '✅ Healthy', degraded: '⚠️ Degraded', unhealthy: '🔴 Unhealthy' };
|
|
1715
|
+
healthLabel.textContent = statusMap[healthData.status] || '❓ Unknown';
|
|
1716
|
+
|
|
1717
|
+
// Stats summary line
|
|
1718
|
+
var t = healthData.totals || {};
|
|
1719
|
+
healthSummary.textContent = (t.activeEntries || 0) + ' entries · ' +
|
|
1720
|
+
(t.handoffs || 0) + ' handoffs · ' +
|
|
1721
|
+
(t.rollups || 0) + ' rollups' +
|
|
1722
|
+
(t.crdtMerges ? ' · 🔄 ' + t.crdtMerges + ' merges' : '');
|
|
1723
|
+
|
|
1724
|
+
// Issue rows
|
|
1725
|
+
var issues = healthData.issues || [];
|
|
1726
|
+
var cleanupBtn = document.getElementById('cleanupBtn');
|
|
1727
|
+
if (issues.length > 0) {
|
|
1728
|
+
var sevIcons = { error: '🔴', warning: '🟡', info: '🔵' };
|
|
1729
|
+
healthIssues.innerHTML = issues.map(function(i) {
|
|
1730
|
+
return '<div class="issue-row">' +
|
|
1731
|
+
'<span>' + (sevIcons[i.severity] || '❓') + '</span>' +
|
|
1732
|
+
'<span>' + escapeHtml(i.message) + '</span>' +
|
|
1733
|
+
'</div>';
|
|
1734
|
+
}).join('');
|
|
1735
|
+
if (cleanupBtn) cleanupBtn.style.display = 'inline-block';
|
|
1736
|
+
} else {
|
|
1737
|
+
healthIssues.innerHTML = '<div style="color:var(--accent-green);font-size:0.8rem">🎉 No issues found</div>';
|
|
1738
|
+
if (cleanupBtn) cleanupBtn.style.display = 'none';
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
healthCard.style.display = 'block';
|
|
1742
|
+
}).catch(function(he) {
|
|
1743
|
+
// Health check not available — silently skip
|
|
1744
|
+
console.warn('Health check unavailable:', he);
|
|
1745
|
+
})
|
|
1746
|
+
|
|
1747
|
+
document.getElementById('content').className = 'grid grid-main fade-in';
|
|
1748
|
+
document.getElementById('content').style.display = 'grid';
|
|
1749
|
+
|
|
1750
|
+
// v3.1: Analytics + Lifecycle Controls + Import
|
|
1751
|
+
document.getElementById('analyticsCard').style.display = 'block';
|
|
1752
|
+
document.getElementById('lifecycleCard').style.display = 'block';
|
|
1753
|
+
document.getElementById('importCard').style.display = 'block';
|
|
1754
|
+
loadAnalytics(project);
|
|
1755
|
+
loadRetention(project).then(function(res) {
|
|
1756
|
+
loadTeam(); // v3.0: auto-load Hivemind team
|
|
1757
|
+
}).catch(function(e) {
|
|
1758
|
+
alert('Failed to load project data: ' + e.message);
|
|
1759
|
+
}) finally {
|
|
1760
|
+
document.getElementById('loading').style.display = 'none';
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// ─── v3.1: Memory Analytics ───────────────────────────────────────────────
|
|
1765
|
+
function loadAnalytics(project) {
|
|
1766
|
+
fetch('/api/analytics?project=' + encodeURIComponent(project));
|
|
1767
|
+
var d = await res.json();
|
|
1768
|
+
|
|
1769
|
+
document.getElementById('astat-entries').textContent = (d.totalEntries || 0);
|
|
1770
|
+
document.getElementById('astat-rollups').textContent = (d.totalRollups || 0);
|
|
1771
|
+
document.getElementById('astat-savings').textContent = (d.rollupSavings || 0);
|
|
1772
|
+
document.getElementById('astat-avglen').textContent = Math.round(d.avgSummaryLength || 0);
|
|
1773
|
+
|
|
1774
|
+
// Sparkline
|
|
1775
|
+
var sparkEl = document.getElementById('sparkline');
|
|
1776
|
+
var days = d.sessionsByDay || [];
|
|
1777
|
+
if (days.length === 0) {
|
|
1778
|
+
// Pad with 14 zero days
|
|
1779
|
+
days = Array.from({length:14}, function(_, i) {
|
|
1780
|
+
var dt = new Date(); dt.setDate(dt.getDate() - (13 - i));
|
|
1781
|
+
return { date: dt.toISOString().slice(0,10), count: 0 };
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
var maxCount = Math.max.apply(null, days.map(function(x){return x.count || 0;})) || 1;
|
|
1785
|
+
sparkEl.innerHTML = days.slice(-14).map(function(d) {
|
|
1786
|
+
var pct = Math.max(4, Math.round(((d.count || 0) / maxCount) * 100));
|
|
1787
|
+
return '<div class="spark-bar" style="height:' + pct + '%" title="' + d.date + ': ' + d.count + '"></div>';
|
|
1788
|
+
}).join('').then(function(res) {
|
|
1789
|
+
|
|
1790
|
+
}).catch(function(e) {
|
|
1791
|
+
console.warn('Analytics load failed:', e);
|
|
1792
|
+
})
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// ─── v3.1: TTL Retention ───────────────────────────────────────────────
|
|
1796
|
+
function loadRetention(project) {
|
|
1797
|
+
fetch('/api/retention?project=' + encodeURIComponent(project));
|
|
1798
|
+
var d = await res.json();
|
|
1799
|
+
var inp = document.getElementById('ttlInput').then(function(res) {
|
|
1800
|
+
if (inp) inp.value = d.ttl_days || 0;
|
|
1801
|
+
}).catch(function(e) {
|
|
1802
|
+
|
|
1803
|
+
})
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
function saveTTL() {
|
|
1807
|
+
var project = document.getElementById('projectSelect').value;
|
|
1808
|
+
if (!project) return;
|
|
1809
|
+
var days = parseInt(document.getElementById('ttlInput').value, 10) || 0;
|
|
1810
|
+
fetch('/api/retention', {
|
|
1811
|
+
method: 'POST',
|
|
1812
|
+
headers: {'Content-Type':'application/json'},
|
|
1813
|
+
body: JSON.stringify({ project, ttl_days: days })
|
|
1814
|
+
});
|
|
1815
|
+
var d = await res.json();
|
|
1816
|
+
if (d.ok) {
|
|
1817
|
+
showFixedToast(days > 0 ? '✓ TTL saved: ' + days + 'd (expired ' + (d.expired || 0) + ')' : '✓ TTL disabled');
|
|
1818
|
+
} else {
|
|
1819
|
+
showToast('❌ ' + (d.error || 'Save failed'), true).then(function(res) {
|
|
1820
|
+
}
|
|
1821
|
+
}).catch(function(e) {
|
|
1822
|
+
showFixedToast('❌ Cannot save TTL', true);
|
|
1823
|
+
})
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// ─── v3.1: Compact Now ───────────────────────────────────────────────
|
|
1827
|
+
function compactNow() {
|
|
1828
|
+
var project = document.getElementById('projectSelect').value;
|
|
1829
|
+
if (!project) return;
|
|
1830
|
+
var btn = document.getElementById('compactBtn');
|
|
1831
|
+
btn.disabled = true;
|
|
1832
|
+
btn.textContent = '🗜️ Compacting...';
|
|
1833
|
+
fetch('/api/compact', {
|
|
1834
|
+
method: 'POST',
|
|
1835
|
+
headers: {'Content-Type':'application/json'},
|
|
1836
|
+
body: JSON.stringify({ project })
|
|
1837
|
+
});
|
|
1838
|
+
var d = await res.json();
|
|
1839
|
+
if (d.ok) {
|
|
1840
|
+
showFixedToast('✓ Compaction done');
|
|
1841
|
+
loadAnalytics(project); // refresh stats
|
|
1842
|
+
} else {
|
|
1843
|
+
showToast('❌ Compaction failed', true).then(function(res) {
|
|
1844
|
+
}
|
|
1845
|
+
}).catch(function(e) {
|
|
1846
|
+
showFixedToast('❌ ' + e.message, true);
|
|
1847
|
+
})
|
|
1848
|
+
finally {
|
|
1849
|
+
btn.disabled = false;
|
|
1850
|
+
btn.textContent = '🗜️ Compact Now';
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// ─── v3.1: PKM Export (Obsidian / Logseq ZIP) ───────────────────────
|
|
1855
|
+
// ── Shared export progress helpers ─────────────────────────────────
|
|
1856
|
+
// Export uses fetch+blob so we can show a building bar during ZIP generation.
|
|
1857
|
+
// Estimated time ~5-15s for most projects (fflate in-memory); staged accordingly.
|
|
1858
|
+
function startExportProgress(isVault) {
|
|
1859
|
+
var wrap = document.getElementById('exportProgressWrap');
|
|
1860
|
+
var bar = document.getElementById('exportProgressBar');
|
|
1861
|
+
var pct = document.getElementById('exportProgressPct');
|
|
1862
|
+
var stage = document.getElementById('exportProgressStage');
|
|
1863
|
+
if (wrap) wrap.style.display = 'block';
|
|
1864
|
+
var stages = isVault
|
|
1865
|
+
? [
|
|
1866
|
+
{ pct: 10, label: 'Fetching ledger entries…', ms: 500 },
|
|
1867
|
+
{ pct: 30, label: 'Rendering Markdown files…', ms: 2000 },
|
|
1868
|
+
{ pct: 55, label: 'Building Wikilink index…', ms: 4000 },
|
|
1869
|
+
{ pct: 75, label: 'Compressing vault ZIP…', ms: 7000 },
|
|
1870
|
+
{ pct: 88, label: 'Finalizing archive…', ms: 11000 },
|
|
1871
|
+
]
|
|
1872
|
+
: [
|
|
1873
|
+
{ pct: 15, label: 'Fetching project data…', ms: 500 },
|
|
1874
|
+
{ pct: 50, label: 'Building archive…', ms: 2000 },
|
|
1875
|
+
{ pct: 80, label: 'Compressing ZIP…', ms: 5000 },
|
|
1876
|
+
{ pct: 92, label: 'Finalizing…', ms: 9000 },
|
|
1877
|
+
];
|
|
1878
|
+
var timers = stages.map(function(s) {
|
|
1879
|
+
return setTimeout(function() {
|
|
1880
|
+
if (bar) bar.style.width = s.pct + '%';
|
|
1881
|
+
if (pct) pct.textContent = s.pct + '%';
|
|
1882
|
+
if (stage) stage.textContent = s.label;
|
|
1883
|
+
}, s.ms);
|
|
1884
|
+
});
|
|
1885
|
+
return timers;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
function finishExportProgress(timers, ok) {
|
|
1889
|
+
timers.forEach(function(t) { clearTimeout(t); });
|
|
1890
|
+
var bar = document.getElementById('exportProgressBar');
|
|
1891
|
+
var pct = document.getElementById('exportProgressPct');
|
|
1892
|
+
var stage = document.getElementById('exportProgressStage');
|
|
1893
|
+
var wrap = document.getElementById('exportProgressWrap');
|
|
1894
|
+
if (bar) bar.classList.add('done');
|
|
1895
|
+
if (bar) bar.style.width = '100%';
|
|
1896
|
+
if (pct) pct.textContent = '100%';
|
|
1897
|
+
if (stage) stage.textContent = ok ? '✅ Ready — downloading…' : '❌ Export failed';
|
|
1898
|
+
setTimeout(function() {
|
|
1899
|
+
if (wrap) wrap.style.display = 'none';
|
|
1900
|
+
if (bar) { bar.classList.remove('done'); bar.style.width = '0%'; }
|
|
1901
|
+
if (pct) pct.textContent = '0%';
|
|
1902
|
+
if (stage) stage.textContent = 'Building archive…';
|
|
1903
|
+
}, 2200);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
// ── v3.1: PKM Export (ZIP) ────────────────────────────────────────
|
|
1907
|
+
function exportPKM() {
|
|
1908
|
+
var project = document.getElementById('projectSelect').value;
|
|
1909
|
+
if (!project) return;
|
|
1910
|
+
var btn = document.getElementById('exportBtn');
|
|
1911
|
+
btn.disabled = true;
|
|
1912
|
+
btn.textContent = '📦 Building…';
|
|
1913
|
+
var timers = startExportProgress(false);
|
|
1914
|
+
fetch('/api/export?project=' + encodeURIComponent(project));
|
|
1915
|
+
if (!res.ok) throw new Error('Server error ' + res.status);
|
|
1916
|
+
var blob = await res.blob();
|
|
1917
|
+
finishExportProgress(timers, true);
|
|
1918
|
+
var url = URL.createObjectURL(blob);
|
|
1919
|
+
var a = document.createElement('a');
|
|
1920
|
+
a.href = url;
|
|
1921
|
+
a.download = 'prism-vault-' + project + '.zip';
|
|
1922
|
+
document.body.appendChild(a);
|
|
1923
|
+
a.click();
|
|
1924
|
+
document.body.removeChild(a);
|
|
1925
|
+
setTimeout(function() { URL.revokeObjectURL(url); }, 10000).then(function(res) {
|
|
1926
|
+
showToast('↓ Download started');
|
|
1927
|
+
}).catch(function(e) {
|
|
1928
|
+
finishExportProgress(timers, false);
|
|
1929
|
+
showFixedToast('❌ Export failed', true);
|
|
1930
|
+
}) finally {
|
|
1931
|
+
btn.disabled = false;
|
|
1932
|
+
btn.textContent = '📦 Export ZIP';
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// ── v6.1: Vault Export (Prism-Port) ────────────────────────────
|
|
1937
|
+
function exportVault() {
|
|
1938
|
+
var project = document.getElementById('projectSelect').value;
|
|
1939
|
+
if (!project) return;
|
|
1940
|
+
var btn = document.getElementById('exportVaultBtn');
|
|
1941
|
+
btn.disabled = true;
|
|
1942
|
+
btn.textContent = '🏛️ Building…';
|
|
1943
|
+
var timers = startExportProgress(true);
|
|
1944
|
+
fetch('/api/export/vault?project=' + encodeURIComponent(project));
|
|
1945
|
+
if (!res.ok) throw new Error('Server error ' + res.status);
|
|
1946
|
+
var blob = await res.blob();
|
|
1947
|
+
finishExportProgress(timers, true);
|
|
1948
|
+
var url = URL.createObjectURL(blob);
|
|
1949
|
+
var a = document.createElement('a');
|
|
1950
|
+
a.href = url;
|
|
1951
|
+
a.download = 'prism-vault-' + project + '.zip';
|
|
1952
|
+
document.body.appendChild(a);
|
|
1953
|
+
a.click();
|
|
1954
|
+
document.body.removeChild(a);
|
|
1955
|
+
setTimeout(function() { URL.revokeObjectURL(url); }, 10000).then(function(res) {
|
|
1956
|
+
showToast('↓ Vault download started — open in Obsidian or Logseq');
|
|
1957
|
+
}).catch(function(e) {
|
|
1958
|
+
finishExportProgress(timers, false);
|
|
1959
|
+
showFixedToast('❌ Vault export failed', true);
|
|
1960
|
+
}) finally {
|
|
1961
|
+
btn.disabled = false;
|
|
1962
|
+
btn.textContent = '🏛️ Export Vault';
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// ─── v5.2: Universal History Import ───────────────────────────────
|
|
1967
|
+
|
|
1968
|
+
// Track the picked file for upload mode
|
|
1969
|
+
var _importPickedFile = null;
|
|
1970
|
+
|
|
1971
|
+
document.getElementById('importFileInput').addEventListener('change', function(e) {
|
|
1972
|
+
var file = e.target.files[0];
|
|
1973
|
+
if (!file) return;
|
|
1974
|
+
_importPickedFile = file;
|
|
1975
|
+
var pathInput = document.getElementById('importPath');
|
|
1976
|
+
pathInput.value = file.name;
|
|
1977
|
+
document.getElementById('importClearBtn').style.display = 'inline-flex';
|
|
1978
|
+
var infoEl = document.getElementById('importFileInfo');
|
|
1979
|
+
var sizeKB = (file.size / 1024).toFixed(1);
|
|
1980
|
+
var sizeMB = (file.size / (1024 * 1024)).toFixed(1);
|
|
1981
|
+
infoEl.textContent = '📄 ' + file.name + ' (' + (file.size > 1048576 ? sizeMB + ' MB' : sizeKB + ' KB') + ')';
|
|
1982
|
+
infoEl.style.display = 'block';
|
|
1983
|
+
|
|
1984
|
+
// Auto-detect format from extension
|
|
1985
|
+
var fmt = document.getElementById('importFormat');
|
|
1986
|
+
if (file.name.endsWith('.jsonl') || file.name.endsWith('.ndjson')) {
|
|
1987
|
+
fmt.value = 'claude';
|
|
1988
|
+
} else if (file.name.toLowerCase().includes('gemini')) {
|
|
1989
|
+
fmt.value = 'gemini';
|
|
1990
|
+
} else if (file.name.toLowerCase().includes('openai') || file.name.toLowerCase().includes('chatgpt')) {
|
|
1991
|
+
fmt.value = 'openai';
|
|
1992
|
+
} else {
|
|
1993
|
+
fmt.value = '';
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
function clearImportFile() {
|
|
1998
|
+
_importPickedFile = null;
|
|
1999
|
+
document.getElementById('importPath').value = '';
|
|
2000
|
+
document.getElementById('importFileInput').value = '';
|
|
2001
|
+
document.getElementById('importClearBtn').style.display = 'none';
|
|
2002
|
+
document.getElementById('importFileInfo').style.display = 'none';
|
|
2003
|
+
document.getElementById('importResult').style.display = 'none';
|
|
2004
|
+
document.getElementById('importFormat').value = '';
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
function runImport(dryRun) {
|
|
2008
|
+
var filePath = document.getElementById('importPath').value.trim();
|
|
2009
|
+
if (!filePath && !_importPickedFile) { showFixedToast('❌ Pick a file or enter a path', true); return; }
|
|
2010
|
+
|
|
2011
|
+
var format = document.getElementById('importFormat').value || undefined;
|
|
2012
|
+
var project = document.getElementById('importProject').value.trim() || undefined;
|
|
2013
|
+
var importBtn = document.getElementById('importBtn');
|
|
2014
|
+
var dryBtn = document.getElementById('importDryBtn');
|
|
2015
|
+
var resultEl = document.getElementById('importResult');
|
|
2016
|
+
var progWrap = document.getElementById('importProgressWrap');
|
|
2017
|
+
var progBar = document.getElementById('importProgressBar');
|
|
2018
|
+
var progPct = document.getElementById('importProgressPct');
|
|
2019
|
+
var progStage = document.getElementById('importProgressStage');
|
|
2020
|
+
|
|
2021
|
+
importBtn.disabled = true;
|
|
2022
|
+
dryBtn.disabled = true;
|
|
2023
|
+
var activeBtn = dryRun ? dryBtn : importBtn;
|
|
2024
|
+
var origText = activeBtn.innerHTML;
|
|
2025
|
+
activeBtn.innerHTML = dryRun ? '🔄 Validating…' : '🔄 Importing…';
|
|
2026
|
+
|
|
2027
|
+
// Hide old result, show progress bar
|
|
2028
|
+
resultEl.style.display = 'none';
|
|
2029
|
+
if (progWrap) progWrap.style.display = 'block';
|
|
2030
|
+
|
|
2031
|
+
// Estimate duration by file size: <500KB~10s, <5MB~30s, else~90s
|
|
2032
|
+
var fileSize = _importPickedFile ? _importPickedFile.size : 0;
|
|
2033
|
+
var estMs = fileSize > 5 * 1024 * 1024 ? 90000
|
|
2034
|
+
: fileSize > 500 * 1024 ? 30000
|
|
2035
|
+
: 10000;
|
|
2036
|
+
|
|
2037
|
+
var importStages = dryRun
|
|
2038
|
+
? [
|
|
2039
|
+
{ pct: 20, label: 'Parsing file structure…', ms: Math.round(estMs * 0.1) },
|
|
2040
|
+
{ pct: 55, label: 'Validating conversation turns…', ms: Math.round(estMs * 0.35) },
|
|
2041
|
+
{ pct: 80, label: 'Checking for duplicates…', ms: Math.round(estMs * 0.65) },
|
|
2042
|
+
{ pct: 92, label: 'Generating preview…', ms: Math.round(estMs * 0.85) },
|
|
2043
|
+
]
|
|
2044
|
+
: [
|
|
2045
|
+
{ pct: 10, label: 'Reading file…', ms: Math.round(estMs * 0.05) },
|
|
2046
|
+
{ pct: 25, label: 'Parsing conversation turns…', ms: Math.round(estMs * 0.15) },
|
|
2047
|
+
{ pct: 45, label: 'Deduplicating entries…', ms: Math.round(estMs * 0.35) },
|
|
2048
|
+
{ pct: 65, label: 'Writing to ledger…', ms: Math.round(estMs * 0.55) },
|
|
2049
|
+
{ pct: 82, label: 'Indexing keywords (FTS5)…', ms: Math.round(estMs * 0.72) },
|
|
2050
|
+
{ pct: 91, label: 'Generating embeddings…', ms: Math.round(estMs * 0.85) },
|
|
2051
|
+
];
|
|
2052
|
+
|
|
2053
|
+
function setImportProgress(pct, label) {
|
|
2054
|
+
if (progBar) progBar.style.width = pct + '%';
|
|
2055
|
+
if (progPct) progPct.textContent = pct + '%';
|
|
2056
|
+
if (progStage) progStage.textContent = label;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
var timers = importStages.map(function(s) {
|
|
2060
|
+
return setTimeout(function() { setImportProgress(s.pct, s.label); }, s.ms);
|
|
2061
|
+
});
|
|
2062
|
+
|
|
2063
|
+
function finishImportProgress(ok, label) {
|
|
2064
|
+
timers.forEach(function(t) { clearTimeout(t); });
|
|
2065
|
+
if (progBar) progBar.classList.add('done');
|
|
2066
|
+
setImportProgress(100, ok ? '✅ ' + (label || 'Done') : '❌ ' + (label || 'Failed'));
|
|
2067
|
+
setTimeout(function() {
|
|
2068
|
+
if (progWrap) progWrap.style.display = 'none';
|
|
2069
|
+
if (progBar) { progBar.classList.remove('done'); progBar.style.width = '0%'; }
|
|
2070
|
+
if (progPct) progPct.textContent = '0%';
|
|
2071
|
+
if (progStage) progStage.textContent = 'Reading file…';
|
|
2072
|
+
}, 2500);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
try {
|
|
2076
|
+
var endpoint, body, headers;
|
|
2077
|
+
|
|
2078
|
+
if (_importPickedFile) {
|
|
2079
|
+
var content = await _importPickedFile.text();
|
|
2080
|
+
endpoint = '/api/import-upload';
|
|
2081
|
+
headers = {'Content-Type':'application/json'};
|
|
2082
|
+
body = JSON.stringify({
|
|
2083
|
+
filename: _importPickedFile.name,
|
|
2084
|
+
content: content,
|
|
2085
|
+
format: format,
|
|
2086
|
+
project: project,
|
|
2087
|
+
dryRun: dryRun
|
|
2088
|
+
});
|
|
2089
|
+
} else {
|
|
2090
|
+
endpoint = '/api/import';
|
|
2091
|
+
headers = {'Content-Type':'application/json'};
|
|
2092
|
+
body = JSON.stringify({ path: filePath, format: format, project: project, dryRun: dryRun });
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
var res = await fetch(endpoint, { method: 'POST', headers: headers, body: body });
|
|
2096
|
+
var d = await res.json();
|
|
2097
|
+
|
|
2098
|
+
if (res.ok && d.ok) {
|
|
2099
|
+
finishImportProgress(true, dryRun ? 'Validation complete' : 'Import complete');
|
|
2100
|
+
resultEl.style.display = 'block';
|
|
2101
|
+
resultEl.style.background = 'rgba(16,185,129,0.1)';
|
|
2102
|
+
resultEl.style.border = '1px solid rgba(16,185,129,0.25)';
|
|
2103
|
+
resultEl.style.color = 'var(--accent-green)';
|
|
2104
|
+
resultEl.innerHTML = '✅ ' + escapeHtml(d.message) +
|
|
2105
|
+
'<div style="margin-top:0.4rem;font-size:0.75rem;color:var(--text-muted)">' +
|
|
2106
|
+
'Conversations: ' + (d.conversationCount || 0) + ' · Turns: ' + (d.successCount || 0) +
|
|
2107
|
+
(d.skipCount ? ' · Skipped: ' + d.skipCount : '') +
|
|
2108
|
+
(d.failCount ? ' · Failed: ' + d.failCount : '') + '</div>';
|
|
2109
|
+
if (!dryRun) { showToast('✓ Import complete'); loadProject(); }
|
|
2110
|
+
} else {
|
|
2111
|
+
finishImportProgress(false, d.error || 'Import failed');
|
|
2112
|
+
resultEl.style.display = 'block';
|
|
2113
|
+
resultEl.style.background = 'rgba(244,63,94,0.1)';
|
|
2114
|
+
resultEl.style.border = '1px solid rgba(244,63,94,0.25)';
|
|
2115
|
+
resultEl.style.color = 'var(--accent-rose)';
|
|
2116
|
+
resultEl.innerHTML = '❌ ' + escapeHtml(d.error || 'Import failed');
|
|
2117
|
+
}
|
|
2118
|
+
} catch(e) {
|
|
2119
|
+
finishImportProgress(false, e.message);
|
|
2120
|
+
resultEl.style.display = 'block';
|
|
2121
|
+
resultEl.style.background = 'rgba(244,63,94,0.1)';
|
|
2122
|
+
resultEl.style.border = '1px solid rgba(244,63,94,0.25)';
|
|
2123
|
+
resultEl.style.color = 'var(--accent-rose)';
|
|
2124
|
+
resultEl.innerHTML = '❌ ' + escapeHtml(e.message);
|
|
2125
|
+
} finally {
|
|
2126
|
+
importBtn.disabled = false;
|
|
2127
|
+
dryBtn.disabled = false;
|
|
2128
|
+
activeBtn.innerHTML = origText;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
function showFixedToast(msg, isErr) {
|
|
2133
|
+
var el = document.getElementById('fixedToast');
|
|
2134
|
+
if (!el) return;
|
|
2135
|
+
el.textContent = msg;
|
|
2136
|
+
el.style.borderColor = isErr ? 'rgba(244,63,94,0.4)' : 'var(--border-glow)';
|
|
2137
|
+
el.style.color = isErr ? 'var(--accent-rose)' : 'var(--text-primary)';
|
|
2138
|
+
el.classList.add('show');
|
|
2139
|
+
clearTimeout(el._t);
|
|
2140
|
+
el._t = setTimeout(function(){ el.classList.remove('show'); }, 3000);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
function escapeHtml(str) {
|
|
2144
|
+
if (!str) return '';
|
|
2145
|
+
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
function formatDate(isoStr) {
|
|
2149
|
+
if (!isoStr) return '';
|
|
2150
|
+
try {
|
|
2151
|
+
var d = new Date(isoStr);
|
|
2152
|
+
return d.toLocaleDateString(undefined, { month:'short', day:'numeric' }) + ' ' +
|
|
2153
|
+
d.toLocaleTimeString(undefined, { hour:'2-digit', minute:'2-digit' });
|
|
2154
|
+
} catch(e) { return isoStr; }
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// Allow Enter key in select to trigger load
|
|
2158
|
+
document.getElementById('projectSelect').addEventListener('change', loadProject);
|
|
2159
|
+
|
|
2160
|
+
// ─── v6.2: Decay View State ───
|
|
2161
|
+
var _decayViewActive = false;
|
|
2162
|
+
function toggleDecayView() {
|
|
2163
|
+
_decayViewActive = !_decayViewActive;
|
|
2164
|
+
var btn = document.getElementById('decayToggle');
|
|
2165
|
+
if (btn) {
|
|
2166
|
+
btn.style.background = _decayViewActive ? 'rgba(139,92,246,0.25)' : 'transparent';
|
|
2167
|
+
btn.style.color = _decayViewActive ? 'var(--accent-purple)' : 'var(--text-secondary)';
|
|
2168
|
+
btn.style.borderColor = _decayViewActive ? 'var(--accent-purple)' : 'rgba(139,92,246,0.3)';
|
|
2169
|
+
}
|
|
2170
|
+
loadGraph();
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
|
|
2174
|
+
/**
|
|
2175
|
+
* Compute decay color for a node.
|
|
2176
|
+
* Fresh (0 days) → bright green (#10b981)
|
|
2177
|
+
* Stale (30+ days) → dim gray (#334155)
|
|
2178
|
+
* Graduated nodes (importance >= 7) stay vibrant purple regardless of age.
|
|
2179
|
+
*/
|
|
2180
|
+
function getDecayColor(daysSince, decayedImportance, group, baseImportance) {
|
|
2181
|
+
// Graduated nodes: always vibrant (check BASE importance, not decayed)
|
|
2182
|
+
if (baseImportance !== null && baseImportance !== undefined && baseImportance >= 7) {
|
|
2183
|
+
return { bg: '#8b5cf6', border: '#7c3aed', fontColor: '#f1f5f9' };
|
|
2184
|
+
}
|
|
2185
|
+
// Clamp days to 0-60 range for interpolation
|
|
2186
|
+
var d = Math.min(60, Math.max(0, daysSince || 0));
|
|
2187
|
+
var t = d / 60; // 0 = fresh, 1 = stale
|
|
2188
|
+
// Interpolate: green → amber → gray
|
|
2189
|
+
var r, g, b;
|
|
2190
|
+
if (t < 0.5) {
|
|
2191
|
+
// green (#10b981) → amber (#f59e0b)
|
|
2192
|
+
var tt = t * 2;
|
|
2193
|
+
r = Math.round(16 + (245 - 16) * tt);
|
|
2194
|
+
g = Math.round(185 + (158 - 185) * tt);
|
|
2195
|
+
b = Math.round(129 + (11 - 129) * tt);
|
|
2196
|
+
} else {
|
|
2197
|
+
// amber (#f59e0b) → gray (#334155)
|
|
2198
|
+
var tt = (t - 0.5) * 2;
|
|
2199
|
+
r = Math.round(245 + (51 - 245) * tt);
|
|
2200
|
+
g = Math.round(158 + (65 - 158) * tt);
|
|
2201
|
+
b = Math.round(11 + (85 - 11) * tt);
|
|
2202
|
+
}
|
|
2203
|
+
var hex = '#' + [r, g, b].map(function(c) { return c.toString(16).padStart(2, '0'); }).join('');
|
|
2204
|
+
var fontBrightness = (t < 0.7) ? '#0f172a' : '#94a3b8';
|
|
2205
|
+
return { bg: hex, border: hex, fontColor: fontBrightness };
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// ─── Neural Graph (v2.3.0 / v5.1 / v6.2 Decay Heatmap) ───
|
|
2209
|
+
function loadGraph() {
|
|
2210
|
+
var container = document.getElementById('network-container');
|
|
2211
|
+
if (!container) return;
|
|
2212
|
+
|
|
2213
|
+
var proj = document.getElementById('graphProjectFilter') ? document.getElementById('graphProjectFilter').value : '';
|
|
2214
|
+
var days = document.getElementById('graphDaysFilter') ? document.getElementById('graphDaysFilter').value : '';
|
|
2215
|
+
var imp = document.getElementById('graphImportanceFilter') ? document.getElementById('graphImportanceFilter').value : '';
|
|
2216
|
+
|
|
2217
|
+
var qs = [];
|
|
2218
|
+
if (proj) qs.push('project=' + encodeURIComponent(proj));
|
|
2219
|
+
if (days) qs.push('days=' + encodeURIComponent(days));
|
|
2220
|
+
if (imp) qs.push('min_importance=' + encodeURIComponent(imp));
|
|
2221
|
+
var url = '/api/graph' + (qs.length ? '?' + qs.join('&') : '');
|
|
2222
|
+
|
|
2223
|
+
fetch(url).then(function(res) { return res.json(); }).then(function(data) {
|
|
2224
|
+
// Empty state — no ledger entries yet
|
|
2225
|
+
if (data.nodes.length === 0) {
|
|
2226
|
+
container.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:0.85rem">No knowledge associations found yet.</div>';
|
|
2227
|
+
var dens = document.getElementById('densityStatContainer');
|
|
2228
|
+
if (dens) dens.style.display = 'none';
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// Calculate Memory Density before truncation
|
|
2233
|
+
var graduatedNodes = data.nodes.filter(function(n) { return (n.value || 0) >= 7; }).length;
|
|
2234
|
+
var denPercentage = Math.round((graduatedNodes / data.nodes.length) * 100);
|
|
2235
|
+
var dens = document.getElementById('densityStatContainer');
|
|
2236
|
+
if (dens) {
|
|
2237
|
+
dens.style.display = 'block';
|
|
2238
|
+
dens.innerHTML = '<strong>Memory Density:</strong> ' + denPercentage + '% <span title="Ratio of Highly-Reinforced (Graduated) knowledge vs raw concepts" style="cursor:help">🧠</span> (' + graduatedNodes + ' / ' + data.nodes.length + ' ideas graduated)';
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// Safety cap: Vis.js Barnes-Hut physics blows the call stack at ~400+ nodes.
|
|
2242
|
+
var MAX_NODES = 200;
|
|
2243
|
+
if (data.nodes.length > MAX_NODES) {
|
|
2244
|
+
var priority = { project: 0, category: 1, keyword: 2 };
|
|
2245
|
+
data.nodes.sort(function(a, b) { return (priority[a.group] || 9) - (priority[b.group] || 9); });
|
|
2246
|
+
var kept = new Set(data.nodes.slice(0, MAX_NODES).map(function(n) { return n.id; }));
|
|
2247
|
+
data.nodes = data.nodes.slice(0, MAX_NODES);
|
|
2248
|
+
data.edges = data.edges.filter(function(e) { return kept.has(e.from) && kept.has(e.to); });
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
// ── v6.2: Apply decay heatmap coloring when toggle is active ──
|
|
2252
|
+
if (_decayViewActive) {
|
|
2253
|
+
data.nodes.forEach(function(n) {
|
|
2254
|
+
var dc = getDecayColor(n.days_since_access, n.decayed_importance, n.group, n.base_importance);
|
|
2255
|
+
n.color = { background: dc.bg, border: dc.border };
|
|
2256
|
+
n.font = { color: dc.fontColor, face: 'Inter', size: n.group === 'project' ? 14 : (n.group === 'category' ? 12 : 10) };
|
|
2257
|
+
// Add decay tooltip
|
|
2258
|
+
var daysText = (n.days_since_access !== null && n.days_since_access !== undefined)
|
|
2259
|
+
? n.days_since_access + 'd ago'
|
|
2260
|
+
: 'unknown';
|
|
2261
|
+
var decayText = (n.decayed_importance !== null && n.decayed_importance !== undefined)
|
|
2262
|
+
? ' · importance: ' + n.decayed_importance
|
|
2263
|
+
: '';
|
|
2264
|
+
n.title = n.label + ' (' + daysText + decayText + ')';
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// Vis.js dark-theme config matching the glassmorphism palette
|
|
2269
|
+
var options = {
|
|
2270
|
+
nodes: {
|
|
2271
|
+
shape: 'dot',
|
|
2272
|
+
borderWidth: 0,
|
|
2273
|
+
font: { color: '#94a3b8', face: 'Inter', size: 12 }
|
|
2274
|
+
},
|
|
2275
|
+
edges: {
|
|
2276
|
+
width: 1,
|
|
2277
|
+
color: { color: 'rgba(139,92,246,0.15)', highlight: '#8b5cf6' },
|
|
2278
|
+
smooth: { type: 'continuous' }
|
|
2279
|
+
},
|
|
2280
|
+
groups: {
|
|
2281
|
+
project: {
|
|
2282
|
+
color: { background: '#8b5cf6', border: '#7c3aed' },
|
|
2283
|
+
size: 20,
|
|
2284
|
+
font: { size: 14, color: '#f1f5f9', face: 'Inter' }
|
|
2285
|
+
},
|
|
2286
|
+
category: {
|
|
2287
|
+
color: { background: '#06b6d4', border: '#0891b2' },
|
|
2288
|
+
size: 10,
|
|
2289
|
+
shape: 'diamond'
|
|
2290
|
+
},
|
|
2291
|
+
keyword: {
|
|
2292
|
+
color: { background: '#1e293b', border: '#334155' },
|
|
2293
|
+
size: 6,
|
|
2294
|
+
font: { size: 10, color: '#64748b' }
|
|
2295
|
+
}
|
|
2296
|
+
},
|
|
2297
|
+
physics: {
|
|
2298
|
+
stabilization: { iterations: 50 },
|
|
2299
|
+
barnesHut: {
|
|
2300
|
+
gravitationalConstant: -3000,
|
|
2301
|
+
springConstant: 0.04,
|
|
2302
|
+
springLength: 80
|
|
2303
|
+
}
|
|
2304
|
+
},
|
|
2305
|
+
interaction: { hover: true, tooltipDelay: 200 }
|
|
2306
|
+
};
|
|
2307
|
+
|
|
2308
|
+
// Create the network visualization
|
|
2309
|
+
var network = new vis.Network(container, data, options);
|
|
2310
|
+
|
|
2311
|
+
// v5.1: Click-to-filter — click a node to isolate its connections
|
|
2312
|
+
var allNodes = data.nodes;
|
|
2313
|
+
var allEdges = data.edges;
|
|
2314
|
+
var isFiltered = false;
|
|
2315
|
+
|
|
2316
|
+
network.on('click', function(params) {
|
|
2317
|
+
if (params.nodes.length === 0) {
|
|
2318
|
+
// Click on empty space — reset the graph if filtered
|
|
2319
|
+
if (isFiltered) {
|
|
2320
|
+
network.setData({ nodes: allNodes, edges: allEdges });
|
|
2321
|
+
isFiltered = false;
|
|
2322
|
+
}
|
|
2323
|
+
var panel = document.getElementById('nodeEditorPanel');
|
|
2324
|
+
if (panel) panel.style.display = 'none';
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
var clickedId = params.nodes[0];
|
|
2329
|
+
|
|
2330
|
+
// Display Node Editor Panel for keywords and categories
|
|
2331
|
+
var nodeData = allNodes.find(function(n) { return n.id === clickedId; });
|
|
2332
|
+
if (nodeData && (nodeData.group === 'keyword' || nodeData.group === 'category')) {
|
|
2333
|
+
document.getElementById('nodeEditorTitle').textContent = nodeData.label;
|
|
2334
|
+
document.getElementById('nodeEditorGroup').textContent = nodeData.group;
|
|
2335
|
+
|
|
2336
|
+
var input = document.getElementById('nodeEditorInput');
|
|
2337
|
+
input.value = nodeData.label;
|
|
2338
|
+
input.dataset.oldId = clickedId;
|
|
2339
|
+
input.dataset.group = nodeData.group;
|
|
2340
|
+
|
|
2341
|
+
// Populate merge dropdown
|
|
2342
|
+
var mergeSelect = document.getElementById('nodeMergeSelect');
|
|
2343
|
+
if (mergeSelect) {
|
|
2344
|
+
var sameGroupNodes = allNodes.filter(function(n) { return n.group === nodeData.group && n.id !== clickedId; });
|
|
2345
|
+
sameGroupNodes.sort(function(a, b) { return a.label.localeCompare(b.label); });
|
|
2346
|
+
mergeSelect.innerHTML = '<option value="">-- Select node to merge into --</option>' +
|
|
2347
|
+
sameGroupNodes.map(function(n) { return '<option value="' + escapeHtml(n.label) + '">' + escapeHtml(n.label) + '</option>'; }).join('');
|
|
2348
|
+
mergeSelect.value = "";
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
var tmBtn = document.getElementById('testMeBtn');
|
|
2352
|
+
var tmCont = document.getElementById('testMeContainer');
|
|
2353
|
+
if (tmCont) tmCont.innerHTML = '';
|
|
2354
|
+
if (tmBtn) {
|
|
2355
|
+
tmBtn.disabled = false;
|
|
2356
|
+
tmBtn.textContent = '📝 Test Me';
|
|
2357
|
+
tmBtn.style.opacity = '1';
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
document.getElementById('nodeEditorPanel').style.display = 'block';
|
|
2361
|
+
} else {
|
|
2362
|
+
var panel = document.getElementById('nodeEditorPanel');
|
|
2363
|
+
if (panel) panel.style.display = 'none';
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// Find all connected edges and nodes
|
|
2367
|
+
var connectedEdges = allEdges.filter(function(e) {
|
|
2368
|
+
return e.from === clickedId || e.to === clickedId;
|
|
2369
|
+
});
|
|
2370
|
+
var connectedNodeIds = new Set([clickedId]);
|
|
2371
|
+
connectedEdges.forEach(function(e) {
|
|
2372
|
+
connectedNodeIds.add(e.from);
|
|
2373
|
+
connectedNodeIds.add(e.to);
|
|
2374
|
+
});
|
|
2375
|
+
var connectedNodes = allNodes.filter(function(n) {
|
|
2376
|
+
return connectedNodeIds.has(n.id);
|
|
2377
|
+
});
|
|
2378
|
+
|
|
2379
|
+
// Show only the clicked node and its neighbors
|
|
2380
|
+
network.setData({ nodes: connectedNodes, edges: connectedEdges });
|
|
2381
|
+
isFiltered = true;
|
|
2382
|
+
});
|
|
2383
|
+
|
|
2384
|
+
// Double-click to reset
|
|
2385
|
+
network.on('doubleClick', function() {
|
|
2386
|
+
network.setData({ nodes: allNodes, edges: allEdges });
|
|
2387
|
+
isFiltered = false;
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2390
|
+
// Show node count in the card title area
|
|
2391
|
+
var graphTitle = container.parentElement.querySelector('.card-title');
|
|
2392
|
+
if (graphTitle) {
|
|
2393
|
+
var statsSpan = graphTitle.querySelector('.graph-stats');
|
|
2394
|
+
if (!statsSpan) {
|
|
2395
|
+
statsSpan = document.createElement('span');
|
|
2396
|
+
statsSpan.className = 'graph-stats';
|
|
2397
|
+
statsSpan.style.cssText = 'margin-left:auto;font-size:0.7rem;color:var(--text-muted);font-family:var(--font-mono);font-weight:400;text-transform:none;letter-spacing:0';
|
|
2398
|
+
graphTitle.appendChild(statsSpan);
|
|
2399
|
+
}
|
|
2400
|
+
var projectCount = allNodes.filter(function(n) { return n.group === 'project'; }).length;
|
|
2401
|
+
var kwCount = allNodes.filter(function(n) { return n.group === 'keyword'; }).length;
|
|
2402
|
+
statsSpan.textContent = projectCount + ' projects · ' + kwCount + ' keywords · ' + allEdges.length + ' edges';
|
|
2403
|
+
}
|
|
2404
|
+
}).catch(function(e) {
|
|
2405
|
+
console.error('Graph error', e);
|
|
2406
|
+
container.innerHTML = '<div style="padding:1rem;color:var(--accent-rose)">Graph failed to load</div>';
|
|
2407
|
+
})
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
function submitNodeEdit() {
|
|
2411
|
+
var input = document.getElementById('nodeEditorInput');
|
|
2412
|
+
var btn = input.nextElementSibling;
|
|
2413
|
+
var newId = input.value.trim();
|
|
2414
|
+
var oldId = input.dataset.oldId;
|
|
2415
|
+
var group = input.dataset.group;
|
|
2416
|
+
|
|
2417
|
+
if (!oldId || !group) return;
|
|
2418
|
+
|
|
2419
|
+
btn.disabled = true;
|
|
2420
|
+
btn.textContent = '...';
|
|
2421
|
+
|
|
2422
|
+
fetch('/api/graph/node', {
|
|
2423
|
+
method: 'POST',
|
|
2424
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2425
|
+
body: JSON.stringify({ oldId: oldId, newId: newId, group: group })
|
|
2426
|
+
});
|
|
2427
|
+
|
|
2428
|
+
if (!res.ok) throw new Error('Failed to update node');
|
|
2429
|
+
|
|
2430
|
+
showFixedToast(newId ? 'Node renamed successfully' : 'Node deleted successfully');
|
|
2431
|
+
document.getElementById('nodeEditorPanel').style.display = 'none';
|
|
2432
|
+
|
|
2433
|
+
// Refresh graph and lists
|
|
2434
|
+
loadGraph();
|
|
2435
|
+
if (document.getElementById('projectSelect').value) {
|
|
2436
|
+
loadSessionList(); // refresh active project view too if one is loaded
|
|
2437
|
+
}
|
|
2438
|
+
} catch (err) {
|
|
2439
|
+
showToast(err.message || 'Error updating node', true);
|
|
2440
|
+
} finally {
|
|
2441
|
+
btn.disabled = false;
|
|
2442
|
+
btn.textContent = 'Apply';
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
function triggerEdgeSynthesis() {
|
|
2447
|
+
var gpf = document.getElementById('graphProjectFilter');
|
|
2448
|
+
var ps = document.getElementById('projectSelect');
|
|
2449
|
+
var project = (gpf ? gpf.value : '') || (ps ? ps.value : '');
|
|
2450
|
+
if (!project) {
|
|
2451
|
+
alert("Please select an active project first.");
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
var btn = document.querySelector('button[onclick="triggerEdgeSynthesis()"]');
|
|
2456
|
+
var status = document.getElementById('synthesisStatus');
|
|
2457
|
+
if (btn) { btn.disabled = true; btn.style.opacity = '0.5'; }
|
|
2458
|
+
if (status) status.textContent = 'running...';
|
|
2459
|
+
|
|
2460
|
+
try {
|
|
2461
|
+
var res = await fetch('/api/graph/synthesize', {
|
|
2462
|
+
method: 'POST',
|
|
2463
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2464
|
+
body: JSON.stringify({ project: project, randomize_selection: true, max_entries: 50 })
|
|
2465
|
+
});
|
|
2466
|
+
|
|
2467
|
+
var data = await res.json();
|
|
2468
|
+
if (res.ok && data.success) {
|
|
2469
|
+
if (status) status.textContent = '✅ Created ' + data.newLinks + ' links (Scanned: ' + data.entriesScanned + ')';
|
|
2470
|
+
setTimeout(loadGraph, 1000); // Reload graph to show new edges
|
|
2471
|
+
loadGraphMetrics(); // Refresh health metrics
|
|
2472
|
+
} else {
|
|
2473
|
+
showFixedToast('❌ Edge Synthesis Error: ' + (data.error || 'Failed'), true);
|
|
2474
|
+
if (status) status.textContent = '❌ Failed';
|
|
2475
|
+
}
|
|
2476
|
+
} catch (e) {
|
|
2477
|
+
showFixedToast('❌ Edge Synthesis Error: ' + e.message, true);
|
|
2478
|
+
if (status) status.textContent = '❌ Error';
|
|
2479
|
+
} finally {
|
|
2480
|
+
if (btn) { btn.disabled = false; btn.style.opacity = '1'; }
|
|
2481
|
+
setTimeout(function() {
|
|
2482
|
+
if (status) status.textContent = '';
|
|
2483
|
+
}, 5000);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
|
|
2488
|
+
|
|
2489
|
+
function triggerCognitiveRoute() {
|
|
2490
|
+
var input = document.getElementById('nodeEditorInput');
|
|
2491
|
+
var state = input && input.dataset && input.dataset.oldId ? input.dataset.oldId : '';
|
|
2492
|
+
var _gpf = document.getElementById('graphProjectFilter');
|
|
2493
|
+
var _ps = document.getElementById('projectSelect');
|
|
2494
|
+
var project = (_gpf ? _gpf.value : '') || (_ps ? _ps.value : '');
|
|
2495
|
+
var container = document.getElementById('cognitiveRouteContainer');
|
|
2496
|
+
var btn = document.getElementById('cognitiveRouteBtn');
|
|
2497
|
+
|
|
2498
|
+
if (!project || !state) {
|
|
2499
|
+
if (container) {
|
|
2500
|
+
container.innerHTML = '<div style="font-size:0.75rem;color:var(--accent-rose);">Select a project and click a graph node first.</div>';
|
|
2501
|
+
}
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
if (btn) {
|
|
2506
|
+
btn.disabled = true;
|
|
2507
|
+
btn.textContent = '...';
|
|
2508
|
+
}
|
|
2509
|
+
if (container) {
|
|
2510
|
+
container.innerHTML = '<div style="font-size:0.75rem;color:var(--text-muted);text-align:center;padding:0.6rem 0;">Resolving cognitive route...</div>';
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
try {
|
|
2514
|
+
var url = '/api/graph/cognitive-route' +
|
|
2515
|
+
'?project=' + encodeURIComponent(project) +
|
|
2516
|
+
'&state=' + encodeURIComponent('State:' + state) +
|
|
2517
|
+
'&role=' + encodeURIComponent('Role:dev') +
|
|
2518
|
+
'&action=' + encodeURIComponent('Action:inspect') +
|
|
2519
|
+
'&explain=true';
|
|
2520
|
+
|
|
2521
|
+
var res = await fetch(url);
|
|
2522
|
+
var data = await res.json();
|
|
2523
|
+
|
|
2524
|
+
if (!res.ok || data.isError) {
|
|
2525
|
+
if (container) {
|
|
2526
|
+
container.innerHTML = '<div style="font-size:0.75rem;color:var(--accent-rose);">' + escapeHtml(data.error || data.text || 'Cognitive route failed') + '</div>';
|
|
2527
|
+
}
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
if (container) {
|
|
2532
|
+
var txt = data.text || '';
|
|
2533
|
+
container.innerHTML = '<pre style="margin:0;white-space:pre-wrap;font-size:0.72rem;line-height:1.45;background:var(--bg-secondary);border:1px solid var(--border-subtle);border-radius:6px;padding:0.6rem;color:var(--text-secondary);">' + escapeHtml(txt) + '</pre>';
|
|
2534
|
+
}
|
|
2535
|
+
} catch (err) {
|
|
2536
|
+
if (container) {
|
|
2537
|
+
container.innerHTML = '<div style="font-size:0.75rem;color:var(--accent-rose);">' + escapeHtml(err.message || 'Route error') + '</div>';
|
|
2538
|
+
}
|
|
2539
|
+
} finally {
|
|
2540
|
+
if (btn) {
|
|
2541
|
+
btn.disabled = false;
|
|
2542
|
+
btn.textContent = '🧭 Route';
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
function triggerTestMe() {
|
|
2548
|
+
var input = document.getElementById('nodeEditorInput');
|
|
2549
|
+
var oldId = input.dataset.oldId;
|
|
2550
|
+
var _gpf = document.getElementById('graphProjectFilter');
|
|
2551
|
+
var _ps = document.getElementById('projectSelect');
|
|
2552
|
+
var project = (_gpf ? _gpf.value : '') || (_ps ? _ps.value : '');
|
|
2553
|
+
|
|
2554
|
+
if (!oldId || !project) return;
|
|
2555
|
+
|
|
2556
|
+
var btn = document.getElementById('testMeBtn');
|
|
2557
|
+
var container = document.getElementById('testMeContainer');
|
|
2558
|
+
if (btn) {
|
|
2559
|
+
btn.disabled = true;
|
|
2560
|
+
btn.textContent = '...';
|
|
2561
|
+
}
|
|
2562
|
+
if (container) {
|
|
2563
|
+
container.innerHTML = '<div style="font-size:0.75rem; color:var(--text-muted); text-align:center; padding:1rem 0;">Generating questions...</div>';
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
try {
|
|
2567
|
+
var res = await fetch('/api/graph/test-me?id=' + encodeURIComponent(oldId) + '&project=' + encodeURIComponent(project));
|
|
2568
|
+
var data = await res.json();
|
|
2569
|
+
|
|
2570
|
+
if (data.reason === 'no_api_key') {
|
|
2571
|
+
if (btn) {
|
|
2572
|
+
btn.disabled = true;
|
|
2573
|
+
btn.title = 'Requires AI key to generate quizzes';
|
|
2574
|
+
btn.style.opacity = '0.5';
|
|
2575
|
+
}
|
|
2576
|
+
if (container) container.innerHTML = '';
|
|
2577
|
+
return;
|
|
2578
|
+
} else if (data.reason === 'generation_failed' || !data.questions || data.questions.length === 0) {
|
|
2579
|
+
showFixedToast('Failed to generate quizzes. Try again.', true);
|
|
2580
|
+
if (container) container.innerHTML = '';
|
|
2581
|
+
if (btn) {
|
|
2582
|
+
btn.disabled = false;
|
|
2583
|
+
btn.textContent = '📝 Test Me';
|
|
2584
|
+
}
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
if (container) {
|
|
2589
|
+
container.innerHTML = '';
|
|
2590
|
+
data.questions.forEach(function(qa) {
|
|
2591
|
+
var card = document.createElement('div');
|
|
2592
|
+
card.style.background = 'var(--bg-secondary)';
|
|
2593
|
+
card.style.border = '1px solid var(--border-subtle)';
|
|
2594
|
+
card.style.borderRadius = '6px';
|
|
2595
|
+
card.style.padding = '0.6rem';
|
|
2596
|
+
|
|
2597
|
+
card.innerHTML =
|
|
2598
|
+
'<div style="font-size:0.8rem; font-weight:600; color:var(--text-primary); margin-bottom:0.4rem;">' + escapeHtml(qa.q) + '</div>' +
|
|
2599
|
+
'<div class="testme-ans" style="display:none; font-size:0.75rem; color:var(--text-secondary); margin-top:0.4rem; padding-top:0.4rem; border-top:1px dashed var(--border-subtle);">' +
|
|
2600
|
+
escapeHtml(qa.a) +
|
|
2601
|
+
'</div>' +
|
|
2602
|
+
'<button onclick="this.previousElementSibling.style.display='block'; this.style.display='none'" style="background:transparent; border:none; color:var(--accent-purple); font-size:0.7rem; cursor:pointer; padding:0; margin-top:0.3rem;">Show Answer</button>';
|
|
2603
|
+
|
|
2604
|
+
container.appendChild(card);
|
|
2605
|
+
}).then(function(res) {
|
|
2606
|
+
}
|
|
2607
|
+
}).catch(function(err) {
|
|
2608
|
+
showFixedToast('Error generating quiz', true);
|
|
2609
|
+
if (container) container.innerHTML = '';
|
|
2610
|
+
}) finally {
|
|
2611
|
+
if (btn && !(btn.title && btn.title.includes('Requires AI key'))) {
|
|
2612
|
+
btn.textContent = '📝 Test Me';
|
|
2613
|
+
btn.disabled = false;
|
|
2614
|
+
}
|
|
2615
|
+
loadGraphMetrics(); // Refresh health metrics
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
// Initialize the graph on page load
|
|
2620
|
+
loadGraph();
|
|
2621
|
+
|
|
2622
|
+
// ─── Settings Modal (v3.0) ───
|
|
2623
|
+
function openSettings() {
|
|
2624
|
+
document.getElementById('settingsModal').classList.add('active');
|
|
2625
|
+
loadSettings();
|
|
2626
|
+
}
|
|
2627
|
+
function closeSettings() {
|
|
2628
|
+
document.getElementById('settingsModal').classList.remove('active');
|
|
2629
|
+
}
|
|
2630
|
+
// Close on overlay click
|
|
2631
|
+
document.getElementById('settingsModal').addEventListener('click', function(e) {
|
|
2632
|
+
if (e.target === this) closeSettings();
|
|
2633
|
+
});
|
|
2634
|
+
|
|
2635
|
+
// ─── Skills Tab JS ───────────────────────────────────────────
|
|
2636
|
+
var _skillsCache = {}; // role → content cache
|
|
2637
|
+
|
|
2638
|
+
function switchSettingsTab(tab) {
|
|
2639
|
+
['settings','skills','providers','observability'].forEach(function(t) {
|
|
2640
|
+
document.getElementById('stab-' + t).classList.toggle('active', t === tab);
|
|
2641
|
+
document.getElementById('spanel-' + t).classList.toggle('active', t === tab);
|
|
2642
|
+
});
|
|
2643
|
+
if (tab === 'skills') {
|
|
2644
|
+
var role = document.getElementById('skillRoleSelect').value;
|
|
2645
|
+
loadSkillForRole(role);
|
|
2646
|
+
}
|
|
2647
|
+
if (tab === 'providers') {
|
|
2648
|
+
loadAiProviderSettings();
|
|
2649
|
+
}
|
|
2650
|
+
if (tab === 'observability') {
|
|
2651
|
+
loadOtelSettings();
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
function loadSkillForRole(role) {
|
|
2656
|
+
fetch('/api/skills').then(function(res) { return res.json(); }).then(function(data) {
|
|
2657
|
+
_skillsCache = data.skills || {};
|
|
2658
|
+
var content = _skillsCache[role] || '';
|
|
2659
|
+
var ta = document.getElementById('skillTextarea');
|
|
2660
|
+
ta.value = content;
|
|
2661
|
+
document.getElementById('skillCharCount').textContent = content.length + ' chars';
|
|
2662
|
+
}).catch(function(e) {
|
|
2663
|
+
console.warn('Skills load failed:', e);
|
|
2664
|
+
})
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
function saveCurrentSkill() {
|
|
2668
|
+
var role = document.getElementById('skillRoleSelect').value;
|
|
2669
|
+
var content = document.getElementById('skillTextarea').value;
|
|
2670
|
+
try {
|
|
2671
|
+
await fetch('/api/skills', {
|
|
2672
|
+
method: 'POST',
|
|
2673
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2674
|
+
body: JSON.stringify({ role: role, content: content })
|
|
2675
|
+
});
|
|
2676
|
+
_skillsCache[role] = content;
|
|
2677
|
+
showFixedToast('✅ Skill saved for ' + role, true);
|
|
2678
|
+
} catch(e) { showFixedToast('❌ Save failed', false); }
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
function clearCurrentSkill() {
|
|
2682
|
+
var role = document.getElementById('skillRoleSelect').value;
|
|
2683
|
+
try {
|
|
2684
|
+
await fetch('/api/skills/' + role, { method: 'DELETE' });
|
|
2685
|
+
document.getElementById('skillTextarea').value = '';
|
|
2686
|
+
document.getElementById('skillCharCount').textContent = '0 chars';
|
|
2687
|
+
_skillsCache[role] = '';
|
|
2688
|
+
showFixedToast('🗑️ Skill cleared for ' + role, true);
|
|
2689
|
+
} catch(e) { showFixedToast('❌ Clear failed', false); }
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
function handleSkillUpload(input) {
|
|
2693
|
+
var file = input.files[0];
|
|
2694
|
+
if (!file) return;
|
|
2695
|
+
var reader = new FileReader();
|
|
2696
|
+
reader.onload = async function(e) {
|
|
2697
|
+
var content = e.target.result;
|
|
2698
|
+
var ta = document.getElementById('skillTextarea');
|
|
2699
|
+
ta.value = content;
|
|
2700
|
+
document.getElementById('skillCharCount').textContent = content.length + ' chars';
|
|
2701
|
+
// Auto-save after upload
|
|
2702
|
+
await saveCurrentSkill();
|
|
2703
|
+
};
|
|
2704
|
+
reader.readAsText(file);
|
|
2705
|
+
input.value = ''; // reset so same file can be re-uploaded
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// ─── AI Providers Settings (v4.4) ────────────────────────────────────
|
|
2709
|
+
// text_provider → governs generateText() (gemini | openai | anthropic)
|
|
2710
|
+
// embedding_provider → governs generateEmbedding() (auto | gemini | openai)
|
|
2711
|
+
|
|
2712
|
+
// Called when the TEXT provider dropdown changes.
|
|
2713
|
+
function onTextProviderChange(value) {
|
|
2714
|
+
document.getElementById('provider-fields-gemini').style.display = value === 'gemini' ? '' : 'none';
|
|
2715
|
+
document.getElementById('provider-fields-openai').style.display = value === 'openai' ? '' : 'none';
|
|
2716
|
+
document.getElementById('provider-fields-anthropic').style.display = value === 'anthropic' ? '' : 'none';
|
|
2717
|
+
// Refresh the Anthropic warning — its visibility depends on both dropdowns
|
|
2718
|
+
refreshAnthropicWarning(value, document.getElementById('select-embedding-provider').value);
|
|
2719
|
+
saveBootSetting('text_provider', value);
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// Called when the EMBEDDING provider dropdown changes.
|
|
2723
|
+
function onEmbeddingProviderChange(value) {
|
|
2724
|
+
var textVal = document.getElementById('select-text-provider').value;
|
|
2725
|
+
// Show the OpenAI embedding model field only when embedding=openai
|
|
2726
|
+
document.getElementById('embed-fields-openai').style.display = value === 'openai' ? '' : 'none';
|
|
2727
|
+
refreshAnthropicWarning(textVal, value);
|
|
2728
|
+
saveBootSetting('embedding_provider', value);
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// Shows/hides the Anthropic+auto warning.
|
|
2732
|
+
// Warning appears when: text=anthropic AND embedding=auto (auto-bridges to Gemini).
|
|
2733
|
+
function refreshAnthropicWarning(textVal, embedVal) {
|
|
2734
|
+
var show = textVal === 'anthropic' && embedVal === 'auto';
|
|
2735
|
+
document.getElementById('anthropic-embed-warning').style.display = show ? '' : 'none';
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
// Load all AI provider settings from the API and populate fields.
|
|
2739
|
+
// Called lazily when the tab is first activated (not on every modal open).
|
|
2740
|
+
function loadAiProviderSettings() {
|
|
2741
|
+
fetch('/api/settings').then(function(res) { return res.json(); }).then(function(data) {
|
|
2742
|
+
var s = data.settings || {};
|
|
2743
|
+
|
|
2744
|
+
// ── Text provider dropdown ────────────────────────────────────────
|
|
2745
|
+
var textProvider = s.text_provider || 'gemini';
|
|
2746
|
+
var textSel = document.getElementById('select-text-provider');
|
|
2747
|
+
if (textSel) textSel.value = textProvider;
|
|
2748
|
+
document.getElementById('provider-fields-gemini').style.display = textProvider === 'gemini' ? '' : 'none';
|
|
2749
|
+
document.getElementById('provider-fields-openai').style.display = textProvider === 'openai' ? '' : 'none';
|
|
2750
|
+
document.getElementById('provider-fields-anthropic').style.display = textProvider === 'anthropic' ? '' : 'none';
|
|
2751
|
+
|
|
2752
|
+
// ── Embedding provider dropdown ───────────────────────────────────
|
|
2753
|
+
var embedProvider = s.embedding_provider || 'auto';
|
|
2754
|
+
var embedSel = document.getElementById('select-embedding-provider');
|
|
2755
|
+
if (embedSel) embedSel.value = embedProvider;
|
|
2756
|
+
document.getElementById('embed-fields-openai').style.display = embedProvider === 'openai' ? '' : 'none';
|
|
2757
|
+
refreshAnthropicWarning(textProvider, embedProvider);
|
|
2758
|
+
|
|
2759
|
+
// ── Gemini fields ─────────────────────────────────────────────────
|
|
2760
|
+
// Never pre-fill API key values for security — use placeholder hint instead.
|
|
2761
|
+
var gKey = document.getElementById('input-google-api-key');
|
|
2762
|
+
if (gKey) gKey.placeholder = s.GOOGLE_API_KEY ? '(key saved — paste to update)' : 'AIza…';
|
|
2763
|
+
|
|
2764
|
+
// ── Anthropic fields ──────────────────────────────────────────────
|
|
2765
|
+
var aKey = document.getElementById('input-anthropic-api-key');
|
|
2766
|
+
if (aKey) aKey.placeholder = s.anthropic_api_key ? '(key saved — paste to update)' : 'sk-ant-…';
|
|
2767
|
+
var aMod = document.getElementById('input-anthropic-model');
|
|
2768
|
+
if (aMod && s.anthropic_model) aMod.value = s.anthropic_model;
|
|
2769
|
+
|
|
2770
|
+
// ── OpenAI / Ollama fields ────────────────────────────────────────
|
|
2771
|
+
var oKey = document.getElementById('input-openai-api-key');
|
|
2772
|
+
if (oKey) oKey.placeholder = s.openai_api_key ? '(key saved — paste to update)' : 'sk-… (blank for Ollama)';
|
|
2773
|
+
var oUrl = document.getElementById('input-openai-base-url');
|
|
2774
|
+
if (oUrl && s.openai_base_url) oUrl.value = s.openai_base_url;
|
|
2775
|
+
var oMod = document.getElementById('input-openai-model');
|
|
2776
|
+
if (oMod && s.openai_model) oMod.value = s.openai_model;
|
|
2777
|
+
var oEmb = document.getElementById('input-openai-embedding-model');
|
|
2778
|
+
if (oEmb && s.openai_embedding_model) oEmb.value = s.openai_embedding_model;
|
|
2779
|
+
}).catch(function(e) {
|
|
2780
|
+
console.warn('AI provider settings load failed:', e);
|
|
2781
|
+
})
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
|
|
2785
|
+
|
|
2786
|
+
// ─── Auto-Load Checkboxes (v4.1) ─────────────────────────────────
|
|
2787
|
+
function loadAutoloadCheckboxes() {
|
|
2788
|
+
var container = document.getElementById('autoload-checkboxes');
|
|
2789
|
+
if (!container) return;
|
|
2790
|
+
fetch('/api/projects').then(function(projRes) { return projRes.json(); }).then(function(projData) {
|
|
2791
|
+
var projects = projData.projects || [];
|
|
2792
|
+
|
|
2793
|
+
var settRes = await fetch('/api/settings');
|
|
2794
|
+
var settData = await settRes.json();
|
|
2795
|
+
var saved = (settData.settings || {}).autoload_projects || '';
|
|
2796
|
+
var selected = saved.split(',').map(function(s){ return s.trim(); }).filter(Boolean);
|
|
2797
|
+
|
|
2798
|
+
if (projects.length === 0) {
|
|
2799
|
+
container.innerHTML = '<span style="color:var(--text-muted);font-size:0.8rem">No projects found</span>';
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
container.innerHTML = projects.map(function(p) {
|
|
2804
|
+
var checked = selected.indexOf(p) !== -1 ? ' checked' : '';
|
|
2805
|
+
return '<label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:var(--text-primary)">' +
|
|
2806
|
+
'<input type="checkbox" value="' + escapeHtml(p) + '"' + checked +
|
|
2807
|
+
' onchange="onAutoloadToggle()"' +
|
|
2808
|
+
' style="accent-color:var(--accent-purple);cursor:pointer" />' +
|
|
2809
|
+
escapeHtml(p) + '</label>';
|
|
2810
|
+
}).join('');
|
|
2811
|
+
}).catch(function(e) {
|
|
2812
|
+
container.innerHTML = '<span style="color:var(--accent-rose);font-size:0.8rem">Failed to load</span>';
|
|
2813
|
+
})
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
function onAutoloadToggle() {
|
|
2817
|
+
var container = document.getElementById('autoload-checkboxes');
|
|
2818
|
+
if (!container) return;
|
|
2819
|
+
var boxes = container.querySelectorAll('input[type=checkbox]');
|
|
2820
|
+
var selected = [];
|
|
2821
|
+
for (var i = 0; i < boxes.length; i++) {
|
|
2822
|
+
if (boxes[i].checked) selected.push(boxes[i].value);
|
|
2823
|
+
}
|
|
2824
|
+
saveBootSetting('autoload_projects', selected.join(','));
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
// ─── Project Repo Paths (v4.2) ─────────────────────────────────
|
|
2828
|
+
function loadRepoPathInputs() {
|
|
2829
|
+
var container = document.getElementById('repopath-inputs');
|
|
2830
|
+
if (!container) return;
|
|
2831
|
+
fetch('/api/projects').then(function(projRes) { return projRes.json(); }).then(function(projData) {
|
|
2832
|
+
var projects = projData.projects || [];
|
|
2833
|
+
|
|
2834
|
+
var settRes = await fetch('/api/settings');
|
|
2835
|
+
var settData = await settRes.json();
|
|
2836
|
+
var settings = settData.settings || {};
|
|
2837
|
+
|
|
2838
|
+
if (projects.length === 0) {
|
|
2839
|
+
container.innerHTML = '<span style="color:var(--text-muted);font-size:0.8rem">No projects found</span>';
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
container.innerHTML = projects.map(function(p) {
|
|
2844
|
+
var savedPath = settings['repo_path:' + p] || '';
|
|
2845
|
+
return '<div style="display:flex;align-items:center;gap:6px">' +
|
|
2846
|
+
'<span style="min-width:100px;color:var(--text-secondary);font-family:var(--font-mono);font-size:0.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml(p) + '">' + escapeHtml(p) + '</span>' +
|
|
2847
|
+
'<input type="text" value="' + escapeHtml(savedPath) + '"' +
|
|
2848
|
+
' placeholder="/path/to/repo"' +
|
|
2849
|
+
' data-project="' + escapeHtml(p) + '"' +
|
|
2850
|
+
' style="flex:1;min-width:140px;padding:0.2rem 0.4rem;background:var(--bg-primary);color:var(--text-primary);border:1px solid var(--border-glass);border-radius:4px;font-size:0.8rem;font-family:var(--font-mono)"' +
|
|
2851
|
+
' onchange="saveRepoPath(this.dataset.project, this.value)"' +
|
|
2852
|
+
' oninput="clearTimeout(this._t); var self=this; this._t=setTimeout(function(){saveRepoPath(self.dataset.project, self.value)},1200)" />' +
|
|
2853
|
+
'</div>';
|
|
2854
|
+
}).join('');
|
|
2855
|
+
}).catch(function(e) {
|
|
2856
|
+
container.innerHTML = '<span style="color:var(--accent-rose);font-size:0.8rem">Failed to load</span>';
|
|
2857
|
+
})
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
function saveRepoPath(project, path) {
|
|
2861
|
+
await saveSetting('repo_path:' + project, path.trim());
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
function loadSettings() {
|
|
2865
|
+
fetch('/api/settings?t=' + Date.now())
|
|
2866
|
+
.then(function(res) { return res.json(); })
|
|
2867
|
+
.then(function(data) {
|
|
2868
|
+
var s = data.settings || {};
|
|
2869
|
+
// Runtime toggles
|
|
2870
|
+
if (s.auto_capture === 'true') document.getElementById('toggle-auto-capture').classList.add('active');
|
|
2871
|
+
else document.getElementById('toggle-auto-capture').classList.remove('active');
|
|
2872
|
+
// Context depth
|
|
2873
|
+
if (s.default_context_depth) document.getElementById('select-context-depth').value = s.default_context_depth;
|
|
2874
|
+
// Theme
|
|
2875
|
+
if (s.dashboard_theme) {
|
|
2876
|
+
document.getElementById('select-theme').value = s.dashboard_theme;
|
|
2877
|
+
applyTheme(s.dashboard_theme);
|
|
2878
|
+
}
|
|
2879
|
+
// Boot toggles
|
|
2880
|
+
if (s.hivemind_enabled === 'true') document.getElementById('toggle-hivemind').classList.add('active');
|
|
2881
|
+
else document.getElementById('toggle-hivemind').classList.remove('active');
|
|
2882
|
+
if (s.task_router_enabled === 'true') document.getElementById('toggle-task-router').classList.add('active');
|
|
2883
|
+
else document.getElementById('toggle-task-router').classList.remove('active');
|
|
2884
|
+
|
|
2885
|
+
// Storage Backend
|
|
2886
|
+
if (s.PRISM_STORAGE) {
|
|
2887
|
+
document.getElementById('storageBackendSelect').value = s.PRISM_STORAGE;
|
|
2888
|
+
}
|
|
2889
|
+
// Agent Identity
|
|
2890
|
+
if (s.default_role) document.getElementById('select-default-role').value = s.default_role;
|
|
2891
|
+
if (s.agent_name) document.getElementById('input-agent-name').value = s.agent_name;
|
|
2892
|
+
if (s.max_tokens) document.getElementById('input-max-tokens').value = s.max_tokens;
|
|
2893
|
+
// Autoload checkboxes are loaded dynamically
|
|
2894
|
+
loadAutoloadCheckboxes();
|
|
2895
|
+
// Repo path inputs are loaded dynamically
|
|
2896
|
+
loadRepoPathInputs();
|
|
2897
|
+
// OTel settings are loaded dynamically when the tab is first opened,
|
|
2898
|
+
// but also pre-load here so values are ready if user lands on that tab.
|
|
2899
|
+
loadOtelSettings();
|
|
2900
|
+
} catch(e) { console.warn('Settings load failed:', e); }
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
// ─── OTel Settings Hydration (v4.6.0) ────────────────────────────────
|
|
2904
|
+
// Separate loader function so it can be called from both loadSettings()
|
|
2905
|
+
// (pre-warm on modal open) and switchSettingsTab('observability')
|
|
2906
|
+
// (refresh on tab focus, in case settings changed elsewhere).
|
|
2907
|
+
function loadOtelSettings() {
|
|
2908
|
+
fetch('/api/settings').then(function(res) { return res.json(); }).then(function(data) {
|
|
2909
|
+
var s = data.settings || {};
|
|
2910
|
+
|
|
2911
|
+
// Toggle: checked when otel_enabled === 'true'
|
|
2912
|
+
var enabledEl = document.getElementById('input-otel-enabled');
|
|
2913
|
+
if (enabledEl) enabledEl.checked = s.otel_enabled === 'true';
|
|
2914
|
+
|
|
2915
|
+
// OTLP endpoint: fall back to Jaeger default so the field is never blank
|
|
2916
|
+
var endpointEl = document.getElementById('input-otel-endpoint');
|
|
2917
|
+
if (endpointEl) endpointEl.value = s.otel_endpoint || 'http://localhost:4318/v1/traces';
|
|
2918
|
+
|
|
2919
|
+
// Service name: fall back to canonical default
|
|
2920
|
+
var serviceEl = document.getElementById('input-otel-service');
|
|
2921
|
+
if (serviceEl) serviceEl.value = s.otel_service_name || 'prism-mcp-server';
|
|
2922
|
+
}).catch(function(e) {
|
|
2923
|
+
console.warn('OTel settings load failed:', e);
|
|
2924
|
+
})
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
function toggleSetting(key, el) {
|
|
2928
|
+
var isActive = el.classList.toggle('active');
|
|
2929
|
+
saveSetting(key, isActive ? 'true' : 'false').then(function(ok) {
|
|
2930
|
+
if (!ok) el.classList.toggle('active'); // rollback on failure
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
function toggleBootSetting(key, el) {
|
|
2934
|
+
var isActive = el.classList.toggle('active');
|
|
2935
|
+
saveSetting(key, isActive ? 'true' : 'false').then(function(ok) {
|
|
2936
|
+
if (!ok) {
|
|
2937
|
+
el.classList.toggle('active'); // rollback on failure
|
|
2938
|
+
} else {
|
|
2939
|
+
showToast('Saved. Restart your AI client for this to take effect.');
|
|
2940
|
+
}
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
function saveBootSetting(key, value) {
|
|
2944
|
+
saveSetting(key, value).then(function(ok) {
|
|
2945
|
+
if (ok) showToast('Saved. Restart your AI client for this to take effect.');
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
function saveSetting(key, value) {
|
|
2950
|
+
fetch('/api/settings', {
|
|
2951
|
+
method: 'POST',
|
|
2952
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2953
|
+
body: JSON.stringify({ key: key, value: value })
|
|
2954
|
+
});
|
|
2955
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
2956
|
+
if (key === 'dashboard_theme') applyTheme(value);
|
|
2957
|
+
// Refresh identity chip if role or name changed
|
|
2958
|
+
if (key === 'default_role' || key === 'agent_name') loadIdentityChip();
|
|
2959
|
+
showToast('Saved ✓').then(function(res) {
|
|
2960
|
+
return true;
|
|
2961
|
+
}).catch(function(e) {
|
|
2962
|
+
console.error('Setting save failed:', e);
|
|
2963
|
+
showFixedToast('⚠️ Save failed — check server connection', true);
|
|
2964
|
+
return false;
|
|
2965
|
+
})
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
/**
|
|
2969
|
+
* applyTheme — sets the data-theme attribute on <html>
|
|
2970
|
+
* CSS custom properties in [data-theme="..."] blocks
|
|
2971
|
+
* override :root defaults instantly, no page reload needed.
|
|
2972
|
+
*/
|
|
2973
|
+
function applyTheme(theme) {
|
|
2974
|
+
document.documentElement.setAttribute('data-theme', theme || 'dark');
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
function showToast(msg) {
|
|
2978
|
+
var toast = document.getElementById('savedToast');
|
|
2979
|
+
toast.textContent = msg || 'Saved ✓';
|
|
2980
|
+
toast.classList.add('show');
|
|
2981
|
+
setTimeout(function() { toast.classList.remove('show'); }, 2000);
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// ─── Hivemind Radar (v5.3 — Health Watchdog) ───
|
|
2985
|
+
var hivemindRefreshTimer = null;
|
|
2986
|
+
|
|
2987
|
+
function loadTeam() {
|
|
2988
|
+
var project = document.getElementById('projectSelect').value;
|
|
2989
|
+
if (!project) return;
|
|
2990
|
+
var card = document.getElementById('hivemindCard');
|
|
2991
|
+
fetch('/api/team?project=' + encodeURIComponent(project));
|
|
2992
|
+
var data = await res.json();
|
|
2993
|
+
var team = data.team || [];
|
|
2994
|
+
var list = document.getElementById('teamList');
|
|
2995
|
+
if (team.length > 0) {
|
|
2996
|
+
var roleIcons = {dev:'🛠️',qa:'🔍',pm:'📋',lead:'🏗️',security:'🔒',ux:'🎨',cmo:'📢'};
|
|
2997
|
+
var statusColors = {
|
|
2998
|
+
active: '#10b981', stale: '#f59e0b', frozen: '#ef4444',
|
|
2999
|
+
overdue: '#f97316', looping: '#a855f7', idle: '#64748b', shutdown: '#374151'
|
|
3000
|
+
};
|
|
3001
|
+
var statusLabels = {
|
|
3002
|
+
active: '🟢', stale: '🟡', frozen: '🔴',
|
|
3003
|
+
overdue: '⏰', looping: '🔄', idle: '💤', shutdown: '⚫'
|
|
3004
|
+
};
|
|
3005
|
+
list.innerHTML = team.map(function(a) {
|
|
3006
|
+
var icon = roleIcons[a.role] || '🤖';
|
|
3007
|
+
var ago = a.last_heartbeat ? timeAgo(a.last_heartbeat) : '?';
|
|
3008
|
+
var dotColor = statusColors[a.status] || '#64748b';
|
|
3009
|
+
var statusIcon = statusLabels[a.status] || '❓';
|
|
3010
|
+
var loopBadge = (a.loop_count && a.loop_count >= 3)
|
|
3011
|
+
? ' <span style="color:#a855f7;font-size:0.75rem">🔄 ' + a.loop_count + 'x</span>'
|
|
3012
|
+
: '';
|
|
3013
|
+
var dotClass = 'pulse-dot' + (a.status === 'looping' ? ' looping' : '');
|
|
3014
|
+
return '<li class="team-item">' +
|
|
3015
|
+
'<span class="' + dotClass + '" style="background:' + dotColor + '"></span>' +
|
|
3016
|
+
'<span class="team-role">' + icon + ' ' + escapeHtml(a.role) + '</span>' +
|
|
3017
|
+
'<span class="team-status" title="' + (a.status || 'active') + '">' + statusIcon + '</span>' +
|
|
3018
|
+
'<span class="team-task">' + escapeHtml(a.current_task || 'idle') + loopBadge + '</span>' +
|
|
3019
|
+
'<span class="team-heartbeat">' + ago + '</span></li>';
|
|
3020
|
+
}).join('');
|
|
3021
|
+
var healthyCt = team.filter(function(a){ return a.status === 'active' || a.status === 'idle'; }).length;
|
|
3022
|
+
var warnCt = team.length - healthyCt;
|
|
3023
|
+
var summary = team.length + ' agent(s)';
|
|
3024
|
+
if (warnCt > 0) summary += ' | ⚠️ ' + warnCt + ' need attention';
|
|
3025
|
+
summary += ' | 🐝 Watchdog active';
|
|
3026
|
+
list.innerHTML += '<li style="color:var(--text-muted);font-size:0.75rem;text-align:center;padding:0.5rem;border-top:1px solid var(--border)">' + summary + '</li>';
|
|
3027
|
+
card.style.display = 'block';
|
|
3028
|
+
} else {
|
|
3029
|
+
list.innerHTML = '<li style="color:var(--text-muted);font-size:0.85rem;text-align:center;padding:1rem">No active agents on this project.</li>';
|
|
3030
|
+
card.style.display = 'block';
|
|
3031
|
+
}
|
|
3032
|
+
} catch(e) {
|
|
3033
|
+
console.warn('Team load failed:', e);
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// v5.3: Auto-refresh Hivemind Radar every 15s
|
|
3038
|
+
function startHivemindRefresh() {
|
|
3039
|
+
stopHivemindRefresh();
|
|
3040
|
+
hivemindRefreshTimer = setInterval(loadTeam, 15000);
|
|
3041
|
+
}
|
|
3042
|
+
function stopHivemindRefresh() {
|
|
3043
|
+
if (hivemindRefreshTimer) { clearInterval(hivemindRefreshTimer); hivemindRefreshTimer = null; }
|
|
3044
|
+
}
|
|
3045
|
+
if (document.getElementById('hivemindCard')) {
|
|
3046
|
+
startHivemindRefresh();
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
// ─── Background Scheduler Status (v5.4) ───
|
|
3050
|
+
function loadSchedulerStatus() {
|
|
3051
|
+
var el = document.getElementById('schedulerContent');
|
|
3052
|
+
if (!el) return;
|
|
3053
|
+
fetch('/api/scheduler').then(function(res) { return res.json(); }).then(function(data) {
|
|
3054
|
+
if (!data.running) {
|
|
3055
|
+
var offHtml = '<div style="color:var(--text-muted)">⏸ Scheduler not running. Set <code style="font-family:var(--font-mono);font-size:0.75rem">PRISM_SCHEDULER_ENABLED=true</code> to enable.</div>';
|
|
3056
|
+
offHtml += '<div style="margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--border-glass); font-size: 0.85em; color: var(--text-muted);">' +
|
|
3057
|
+
'<strong>Web Scholar:</strong> ' + (data.scholarRunning ? '🟢 Enabled' : '🔴 Disabled') +
|
|
3058
|
+
(data.scholarIntervalMs ? ' (every ' + Math.round(data.scholarIntervalMs / 60000) + 'm)' : '') +
|
|
3059
|
+
'</div>';
|
|
3060
|
+
el.innerHTML = offHtml;
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
var intervalH = Math.round(data.intervalMs / 3600000);
|
|
3064
|
+
var parts = ['<div style="display:flex;gap:0.75rem;flex-wrap:wrap;margin-bottom:0.5rem">'];
|
|
3065
|
+
parts.push('<span style="color:var(--accent-green)">🟢 Running</span>');
|
|
3066
|
+
parts.push('<span>Interval: <strong>' + intervalH + 'h</strong></span>');
|
|
3067
|
+
if (data.startedAt) {
|
|
3068
|
+
parts.push('<span>Started: ' + formatDate(data.startedAt) + '</span>');
|
|
3069
|
+
}
|
|
3070
|
+
parts.push('</div>');
|
|
3071
|
+
|
|
3072
|
+
if (data.lastSweep) {
|
|
3073
|
+
var ls = data.lastSweep;
|
|
3074
|
+
parts.push('<div style="border-top:1px solid var(--border-glass);padding-top:0.5rem;margin-top:0.25rem">');
|
|
3075
|
+
parts.push('<div style="margin-bottom:0.3rem;color:var(--text-secondary)">Last sweep: ' + formatDate(ls.completedAt) + ' (' + ls.durationMs + 'ms)</div>');
|
|
3076
|
+
parts.push('<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:0.3rem;font-size:0.75rem">');
|
|
3077
|
+
var t = ls.tasks;
|
|
3078
|
+
if (t.ttlSweep.ran) {
|
|
3079
|
+
parts.push('<div>🗓️ TTL: ' + t.ttlSweep.totalExpired + ' expired (' + t.ttlSweep.projectsSwept + ' projects)</div>');
|
|
3080
|
+
}
|
|
3081
|
+
if (t.importanceDecay.ran) {
|
|
3082
|
+
parts.push('<div>📉 Decay: ' + t.importanceDecay.projectsDecayed + ' projects</div>');
|
|
3083
|
+
}
|
|
3084
|
+
if (t.compaction.ran) {
|
|
3085
|
+
parts.push('<div>🧹 Compact: ' + t.compaction.projectsCompacted + ' compacted</div>');
|
|
3086
|
+
}
|
|
3087
|
+
if (t.deepPurge.ran) {
|
|
3088
|
+
var bytes = t.deepPurge.reclaimedBytes;
|
|
3089
|
+
var bytesStr = bytes > 1048576 ? (bytes / 1048576).toFixed(1) + 'MB' : bytes > 1024 ? (bytes / 1024).toFixed(1) + 'KB' : bytes + 'B';
|
|
3090
|
+
parts.push('<div>💾 Purge: ' + t.deepPurge.purged + ' entries (' + bytesStr + ' freed)</div>');
|
|
3091
|
+
}
|
|
3092
|
+
parts.push('</div>');
|
|
3093
|
+
// Show errors if any
|
|
3094
|
+
var errors = [t.ttlSweep.error, t.importanceDecay.error, t.compaction.error, t.deepPurge.error].filter(Boolean);
|
|
3095
|
+
if (errors.length > 0) {
|
|
3096
|
+
parts.push('<div style="color:var(--accent-rose);margin-top:0.3rem;font-size:0.7rem">⚠️ ' + errors.join(' | ') + '</div>');
|
|
3097
|
+
}
|
|
3098
|
+
parts.push('</div>');
|
|
3099
|
+
} else {
|
|
3100
|
+
parts.push('<div style="color:var(--text-muted)">No sweep completed yet. First sweep runs 5s after start.</div>');
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
var scholarStatusHtml = '<div style="margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--border-glass); font-size: 0.85em; color: var(--text-muted);">' +
|
|
3104
|
+
'<strong>Web Scholar:</strong> ' + (data.scholarRunning ? '🟢 Enabled' : '🔴 Disabled') +
|
|
3105
|
+
(data.scholarIntervalMs ? ' (every ' + Math.round(data.scholarIntervalMs / 60000) + 'm)' : '') +
|
|
3106
|
+
'</div>';
|
|
3107
|
+
parts.push(scholarStatusHtml);
|
|
3108
|
+
|
|
3109
|
+
el.innerHTML = parts.join('');
|
|
3110
|
+
}).catch(function(e) {
|
|
3111
|
+
el.innerHTML = '<div style="color:var(--text-muted)">Scheduler status unavailable</div>';
|
|
3112
|
+
})
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
function loadGraphMetrics() {
|
|
3116
|
+
var el = document.getElementById('graphMetricsContent');
|
|
3117
|
+
var warn = document.getElementById('graphHealthWarnings');
|
|
3118
|
+
if (!el) return;
|
|
3119
|
+
fetch('/api/graph/metrics').then(function(res) { return res.json(); }).then(function(m) {
|
|
3120
|
+
var parts = [];
|
|
3121
|
+
|
|
3122
|
+
// Synthesis row
|
|
3123
|
+
parts.push('<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.3rem;margin-bottom:0.5rem">');
|
|
3124
|
+
parts.push('<div><strong>Synthesis</strong></div>');
|
|
3125
|
+
parts.push('<div><strong>Test Me</strong></div>');
|
|
3126
|
+
|
|
3127
|
+
// Synthesis stats
|
|
3128
|
+
parts.push('<div style="font-size:0.75rem">');
|
|
3129
|
+
parts.push('Runs: <strong>' + m.synthesis.runs_total + '</strong>');
|
|
3130
|
+
if (m.synthesis.runs_failed > 0) parts.push(' (<span style="color:var(--accent-rose)">' + m.synthesis.runs_failed + ' failed</span>)');
|
|
3131
|
+
parts.push('<br>Links created: <strong>' + m.synthesis.links_created_total + '</strong>');
|
|
3132
|
+
if (m.synthesis.last_run_at) {
|
|
3133
|
+
var synthStatus = m.synthesis.last_status === 'ok'
|
|
3134
|
+
? '<span style="color:var(--accent-green)">✓ ok</span>'
|
|
3135
|
+
: '<span style="color:var(--accent-rose)">✗ error</span>';
|
|
3136
|
+
parts.push('<br>Last: ' + synthStatus + ' (' + m.synthesis.last_links_created + ' links)');
|
|
3137
|
+
parts.push('<br><span style="color:var(--text-muted)">' + timeAgo(m.synthesis.last_run_at) + '</span>');
|
|
3138
|
+
}
|
|
3139
|
+
if (m.synthesis.duration_p50_ms !== null) {
|
|
3140
|
+
parts.push('<br>p50: ' + m.synthesis.duration_p50_ms + 'ms');
|
|
3141
|
+
}
|
|
3142
|
+
parts.push('</div>');
|
|
3143
|
+
|
|
3144
|
+
// Test-Me stats
|
|
3145
|
+
parts.push('<div style="font-size:0.75rem">');
|
|
3146
|
+
parts.push('Requests: <strong>' + m.testMe.requests_total + '</strong>');
|
|
3147
|
+
parts.push('<br><span style="color:var(--accent-green)">✓ ' + m.testMe.success_total + '</span>');
|
|
3148
|
+
if (m.testMe.no_api_key_total > 0) parts.push(' <span style="color:var(--accent-amber)">🔑 ' + m.testMe.no_api_key_total + '</span>');
|
|
3149
|
+
if (m.testMe.generation_failed_total > 0) parts.push(' <span style="color:var(--accent-rose)">✗ ' + m.testMe.generation_failed_total + '</span>');
|
|
3150
|
+
if (m.testMe.last_run_at) {
|
|
3151
|
+
var tmStatus = m.testMe.last_status === 'success'
|
|
3152
|
+
? '<span style="color:var(--accent-green)">✓</span>'
|
|
3153
|
+
: m.testMe.last_status === 'no_api_key'
|
|
3154
|
+
? '<span style="color:var(--accent-amber)">🔑</span>'
|
|
3155
|
+
: '<span style="color:var(--accent-rose)">✗</span>';
|
|
3156
|
+
parts.push('<br>Last: ' + tmStatus + ' ' + m.testMe.last_status);
|
|
3157
|
+
parts.push('<br><span style="color:var(--text-muted)">' + timeAgo(m.testMe.last_run_at) + '</span>');
|
|
3158
|
+
}
|
|
3159
|
+
if (m.testMe.duration_p50_ms !== null) {
|
|
3160
|
+
parts.push('<br>p50: ' + m.testMe.duration_p50_ms + 'ms');
|
|
3161
|
+
}
|
|
3162
|
+
parts.push('</div>');
|
|
3163
|
+
parts.push('</div>');
|
|
3164
|
+
|
|
3165
|
+
// Pruning summary row
|
|
3166
|
+
if (m.pruning && m.pruning.last_run_at) {
|
|
3167
|
+
parts.push('<div style="border-top:1px solid var(--border-glass);padding-top:0.4rem;margin-top:0.2rem;font-size:0.75rem">');
|
|
3168
|
+
parts.push('🧹 Pruning: ' + m.pruning.projects_considered_last + ' considered, ' + m.pruning.projects_pruned_last + ' impacted');
|
|
3169
|
+
parts.push('<br>Links: ' + m.pruning.links_soft_pruned_last + ' soft-pruned / ' + m.pruning.links_scanned_last + ' scanned');
|
|
3170
|
+
var pruneRatio = m.pruning.links_scanned_last > 0
|
|
3171
|
+
? Math.round((m.pruning.links_soft_pruned_last / m.pruning.links_scanned_last) * 100)
|
|
3172
|
+
: 0;
|
|
3173
|
+
parts.push(' (' + pruneRatio + '%)');
|
|
3174
|
+
parts.push('<br>Threshold: ' + m.pruning.min_strength_last + ' | ' + m.pruning.duration_ms_last + 'ms');
|
|
3175
|
+
|
|
3176
|
+
var pruneSkipParts = [];
|
|
3177
|
+
if (m.pruning.skipped_backpressure_last > 0) pruneSkipParts.push('⏳ ' + m.pruning.skipped_backpressure_last + ' backpressure');
|
|
3178
|
+
if (m.pruning.skipped_cooldown_last > 0) pruneSkipParts.push('🕒 ' + m.pruning.skipped_cooldown_last + ' cooldown');
|
|
3179
|
+
if (m.pruning.skipped_budget_last > 0) pruneSkipParts.push('⛽ ' + m.pruning.skipped_budget_last + ' budget');
|
|
3180
|
+
if (pruneSkipParts.length > 0) {
|
|
3181
|
+
parts.push('<br><span style="color:var(--accent-amber)">Skipped: ' + pruneSkipParts.join(' · ') + '</span>');
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
parts.push('<br><span style="color:var(--text-muted)">' + timeAgo(m.pruning.last_run_at) + '</span>');
|
|
3185
|
+
parts.push('</div>');
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// SLO derivations row (WS4)
|
|
3189
|
+
if (m.slo) {
|
|
3190
|
+
parts.push('<div style="border-top:1px solid var(--border-glass);padding-top:0.4rem;margin-top:0.2rem;font-size:0.75rem">');
|
|
3191
|
+
parts.push('<strong>SLO</strong>');
|
|
3192
|
+
|
|
3193
|
+
// Synthesis success rate — color-coded
|
|
3194
|
+
if (m.slo.synthesis_success_rate !== null) {
|
|
3195
|
+
var rate = m.slo.synthesis_success_rate;
|
|
3196
|
+
var ratePct = Math.round(rate * 100);
|
|
3197
|
+
var rateColor = rate >= 0.95 ? 'var(--accent-green)' : rate >= 0.80 ? 'var(--accent-amber)' : 'var(--accent-rose)';
|
|
3198
|
+
parts.push('<br>Success rate: <span style="color:' + rateColor + ';font-weight:600">' + ratePct + '%</span>');
|
|
3199
|
+
} else {
|
|
3200
|
+
parts.push('<br>Success rate: <span style="color:var(--text-muted)">—</span>');
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// Net new links
|
|
3204
|
+
var netNew = m.slo.net_new_links_last_sweep;
|
|
3205
|
+
var netColor = netNew > 0 ? 'var(--accent-green)' : netNew < 0 ? 'var(--accent-rose)' : 'var(--text-muted)';
|
|
3206
|
+
var netSign = netNew > 0 ? '+' : '';
|
|
3207
|
+
parts.push(' · Net links: <span style="color:' + netColor + '">' + netSign + netNew + '</span>');
|
|
3208
|
+
|
|
3209
|
+
// Prune ratio
|
|
3210
|
+
var pruneRatioPct = Math.round(m.slo.prune_ratio_last_sweep * 100);
|
|
3211
|
+
parts.push(' · Prune: ' + pruneRatioPct + '%');
|
|
3212
|
+
|
|
3213
|
+
// Sweep duration
|
|
3214
|
+
if (m.slo.scheduler_sweep_duration_ms_last > 0) {
|
|
3215
|
+
parts.push(' · Sweep: ' + m.slo.scheduler_sweep_duration_ms_last + 'ms');
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
parts.push('</div>');
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
// Cognitive Routing row (v6.5)
|
|
3222
|
+
if (m.cognitive && m.cognitive.evaluations_total > 0) {
|
|
3223
|
+
parts.push('<div style="border-top:1px solid var(--border-glass);padding-top:0.4rem;margin-top:0.2rem;font-size:0.75rem">');
|
|
3224
|
+
parts.push('<strong>🧠 Cognitive Routing</strong>');
|
|
3225
|
+
parts.push('<br>Evaluations: <strong>' + m.cognitive.evaluations_total + '</strong>');
|
|
3226
|
+
|
|
3227
|
+
// Route distribution bar
|
|
3228
|
+
var cogTotal = m.cognitive.evaluations_total;
|
|
3229
|
+
var autoP = Math.round((m.cognitive.route_auto_total / cogTotal) * 100);
|
|
3230
|
+
var clarP = Math.round((m.cognitive.route_clarify_total / cogTotal) * 100);
|
|
3231
|
+
var fallP = 100 - autoP - clarP;
|
|
3232
|
+
parts.push('<div style="display:flex;height:8px;border-radius:4px;overflow:hidden;margin:4px 0;background:var(--surface-glass)">');
|
|
3233
|
+
if (autoP > 0) parts.push('<div style="width:' + autoP + '%;background:var(--accent-green)" title="Auto: ' + autoP + '%"></div>');
|
|
3234
|
+
if (clarP > 0) parts.push('<div style="width:' + clarP + '%;background:var(--accent-amber)" title="Clarify: ' + clarP + '%"></div>');
|
|
3235
|
+
if (fallP > 0) parts.push('<div style="width:' + fallP + '%;background:var(--accent-rose)" title="Fallback: ' + fallP + '%"></div>');
|
|
3236
|
+
parts.push('</div>');
|
|
3237
|
+
parts.push('<span style="color:var(--accent-green)">● Auto ' + autoP + '%</span>');
|
|
3238
|
+
parts.push(' <span style="color:var(--accent-amber)">● Clarify ' + clarP + '%</span>');
|
|
3239
|
+
parts.push(' <span style="color:var(--accent-rose)">● Fallback ' + fallP + '%</span>');
|
|
3240
|
+
|
|
3241
|
+
// Rates
|
|
3242
|
+
if (m.cognitive.ambiguity_rate !== null) {
|
|
3243
|
+
var ambPct = Math.round(m.cognitive.ambiguity_rate * 100);
|
|
3244
|
+
var ambColor = ambPct > 40 ? 'var(--accent-rose)' : ambPct > 20 ? 'var(--accent-amber)' : 'var(--accent-green)';
|
|
3245
|
+
parts.push('<br>Ambiguity: <span style="color:' + ambColor + ';font-weight:600">' + ambPct + '%</span>');
|
|
3246
|
+
}
|
|
3247
|
+
if (m.cognitive.fallback_rate !== null) {
|
|
3248
|
+
var fbPct = Math.round(m.cognitive.fallback_rate * 100);
|
|
3249
|
+
var fbColor = fbPct > 30 ? 'var(--accent-rose)' : fbPct > 15 ? 'var(--accent-amber)' : 'var(--accent-green)';
|
|
3250
|
+
parts.push(' · Fallback: <span style="color:' + fbColor + ';font-weight:600">' + fbPct + '%</span>');
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
// Convergence steps
|
|
3254
|
+
if (m.cognitive.median_convergence_steps !== null) {
|
|
3255
|
+
parts.push('<br>Convergence: ' + m.cognitive.median_convergence_steps + ' steps (avg)');
|
|
3256
|
+
}
|
|
3257
|
+
if (m.cognitive.duration_p50_ms !== null) {
|
|
3258
|
+
parts.push(' · p50: ' + m.cognitive.duration_p50_ms + 'ms');
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
// Last evaluation
|
|
3262
|
+
if (m.cognitive.last_run_at) {
|
|
3263
|
+
var lastRoute = m.cognitive.last_route || '—';
|
|
3264
|
+
var lastConcept = m.cognitive.last_concept || '(none)';
|
|
3265
|
+
var lastConf = m.cognitive.last_confidence !== null ? Math.round(m.cognitive.last_confidence * 100) + '%' : '—';
|
|
3266
|
+
parts.push('<br>Last: ' + lastRoute + ' → ' + lastConcept + ' (' + lastConf + ')');
|
|
3267
|
+
parts.push('<br><span style="color:var(--text-muted)">' + timeAgo(m.cognitive.last_run_at) + '</span>');
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
parts.push('</div>');
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
|
|
3274
|
+
el.innerHTML = parts.join('');
|
|
3275
|
+
|
|
3276
|
+
// Warning badges
|
|
3277
|
+
if (warn) {
|
|
3278
|
+
var badges = [];
|
|
3279
|
+
if (m.warnings.synthesis_quality_warning) {
|
|
3280
|
+
badges.push('<span style="background:var(--accent-amber);color:#000;padding:2px 6px;border-radius:3px;font-size:0.65rem;font-weight:600" title="Over 85% of synthesis candidates are below threshold">⚠ Quality</span>');
|
|
3281
|
+
}
|
|
3282
|
+
if (m.warnings.testme_provider_warning) {
|
|
3283
|
+
badges.push('<span style="background:var(--accent-rose);color:#fff;padding:2px 6px;border-radius:3px;font-size:0.65rem;font-weight:600" title="No API key configured — Test Me cannot generate quizzes">🔑 No Key</span>');
|
|
3284
|
+
}
|
|
3285
|
+
if (m.warnings.synthesis_failure_warning) {
|
|
3286
|
+
badges.push('<span style="background:var(--accent-rose);color:#fff;padding:2px 6px;border-radius:3px;font-size:0.65rem;font-weight:600" title="Over 20% of synthesis runs are failing">⚠ Failures</span>');
|
|
3287
|
+
}
|
|
3288
|
+
if (m.warnings.cognitive_fallback_rate_warning) {
|
|
3289
|
+
badges.push('<span style="background:var(--accent-rose);color:#fff;padding:2px 6px;border-radius:3px;font-size:0.65rem;font-weight:600" title="Over 30% of cognitive routes land on FALLBACK">⚠ Cog Fallback</span>');
|
|
3290
|
+
}
|
|
3291
|
+
if (m.warnings.cognitive_ambiguity_rate_warning) {
|
|
3292
|
+
badges.push('<span style="background:var(--accent-amber);color:#000;padding:2px 6px;border-radius:3px;font-size:0.65rem;font-weight:600" title="Over 40% of cognitive evaluations are ambiguous">⚠ Cog Ambiguity</span>');
|
|
3293
|
+
}
|
|
3294
|
+
warn.innerHTML = badges.join('');
|
|
3295
|
+
}
|
|
3296
|
+
}).catch(function(e) {
|
|
3297
|
+
el.innerHTML = '<div style="color:var(--text-muted)">Graph metrics unavailable</div>';
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
function triggerWebScholar() {
|
|
3302
|
+
var btn = document.getElementById('scholarBtn');
|
|
3303
|
+
if (btn) { btn.disabled = true; btn.textContent = '🔄 Triggering...'; }
|
|
3304
|
+
fetch('/api/scholar/trigger', { method: 'POST' }).then(function(res) { return res.json(); }).then(function(data) {
|
|
3305
|
+
showFixedToast(data.message || (data.ok ? 'Scholar triggered.' : 'Scholar failed.'), data.ok);
|
|
3306
|
+
}).catch(function(e) {
|
|
3307
|
+
showFixedToast('Scholar trigger failed.', false);
|
|
3308
|
+
}) finally {
|
|
3309
|
+
if (btn) { btn.disabled = false; btn.textContent = '🧠 Scholar (Run)'; }
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
// Load scheduler status on page load
|
|
3314
|
+
loadSchedulerStatus();
|
|
3315
|
+
loadGraphMetrics();
|
|
3316
|
+
// Auto-refresh scheduler status every 60s
|
|
3317
|
+
setInterval(loadSchedulerStatus, 60000);
|
|
3318
|
+
setInterval(loadGraphMetrics, 60000);
|
|
3319
|
+
|
|
3320
|
+
function timeAgo(iso) {
|
|
3321
|
+
var diff = Date.now() - new Date(iso).getTime();
|
|
3322
|
+
var mins = Math.floor(diff / 60000);
|
|
3323
|
+
if (mins < 1) return 'just now';
|
|
3324
|
+
if (mins < 60) return mins + 'm ago';
|
|
3325
|
+
return Math.floor(mins/60) + 'h ago';
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
// ─── Brain Health Cleanup (v6.1.4) — with simulated progress bar ───
|
|
3329
|
+
function cleanupIssues() {
|
|
3330
|
+
var btn = document.getElementById('cleanupBtn');
|
|
3331
|
+
var wrap = document.getElementById('healthProgressWrap');
|
|
3332
|
+
var bar = document.getElementById('healthProgressBar');
|
|
3333
|
+
var pctEl = document.getElementById('healthProgressPct');
|
|
3334
|
+
var stageEl = document.getElementById('healthProgressStage');
|
|
3335
|
+
|
|
3336
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Cleaning…'; }
|
|
3337
|
+
|
|
3338
|
+
// ── show progress bar ──
|
|
3339
|
+
if (wrap) wrap.style.display = 'block';
|
|
3340
|
+
|
|
3341
|
+
// Stages mapped to approximate % milestones over ~120s.
|
|
3342
|
+
// Easing: fast early (embedding detection is quick), slow in the
|
|
3343
|
+
// middle (100-iteration embedding backfill loop), normal at the end.
|
|
3344
|
+
var stages = [
|
|
3345
|
+
{ pct: 5, label: 'Running health scan…', ms: 1500 },
|
|
3346
|
+
{ pct: 12, label: 'Identifying missing embeddings…', ms: 4000 },
|
|
3347
|
+
{ pct: 22, label: 'Backfilling embeddings (batch 1)…', ms: 10000 },
|
|
3348
|
+
{ pct: 35, label: 'Backfilling embeddings (batch 2)…', ms: 20000 },
|
|
3349
|
+
{ pct: 48, label: 'Backfilling embeddings (batch 3)…', ms: 30000 },
|
|
3350
|
+
{ pct: 60, label: 'Backfilling embeddings (batch 4)…', ms: 40000 },
|
|
3351
|
+
{ pct: 70, label: 'Backfilling embeddings (batch 5)…', ms: 55000 },
|
|
3352
|
+
{ pct: 78, label: 'Backfilling embeddings (batch 6)…', ms: 70000 },
|
|
3353
|
+
{ pct: 85, label: 'Cleaning orphaned handoffs…', ms: 85000 },
|
|
3354
|
+
{ pct: 90, label: 'Verifying repairs…', ms: 100000 },
|
|
3355
|
+
{ pct: 95, label: 'Finalizing…', ms: 115000 },
|
|
3356
|
+
];
|
|
3357
|
+
|
|
3358
|
+
function setProgress(pct, label) {
|
|
3359
|
+
if (bar) { bar.style.width = pct + '%'; }
|
|
3360
|
+
if (pctEl) { pctEl.textContent = pct + '%'; }
|
|
3361
|
+
if (stageEl && label) { stageEl.textContent = label; }
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
// Kick off all stage timers
|
|
3365
|
+
var timers = stages.map(function(s) {
|
|
3366
|
+
return setTimeout(function() { setProgress(s.pct, s.label); }, s.ms);
|
|
3367
|
+
});
|
|
3368
|
+
|
|
3369
|
+
function clearTimers() { timers.forEach(function(t) { clearTimeout(t); }); }
|
|
3370
|
+
|
|
3371
|
+
function finishProgress(ok, label) {
|
|
3372
|
+
clearTimers();
|
|
3373
|
+
if (bar) bar.classList.add('done');
|
|
3374
|
+
setProgress(100, ok ? '✅ Repair complete' : '❌ ' + (label || 'Repair failed'));
|
|
3375
|
+
// hide bar after a short celebration
|
|
3376
|
+
setTimeout(function() {
|
|
3377
|
+
if (wrap) wrap.style.display = 'none';
|
|
3378
|
+
if (bar) { bar.classList.remove('done'); bar.style.width = '0%'; }
|
|
3379
|
+
if (pctEl) pctEl.textContent = '0%';
|
|
3380
|
+
}, 2500);
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
try {
|
|
3384
|
+
fetch('/api/health/cleanup', { method: 'POST' }).then(function(res) { return res.json(); }).then(function(data) {
|
|
3385
|
+
var msg = data.message || data.error || (data.ok ? 'Cleanup complete.' : 'Cleanup failed.');
|
|
3386
|
+
finishProgress(data.ok, msg);
|
|
3387
|
+
showFixedToast(msg, data.ok);
|
|
3388
|
+
if (btn) { btn.disabled = false; btn.textContent = '🧹 Fix Issues'; }
|
|
3389
|
+
// Re-run health check to refresh the card
|
|
3390
|
+
setTimeout(function() {
|
|
3391
|
+
try {
|
|
3392
|
+
var healthRes = await fetch('/api/health');
|
|
3393
|
+
var healthData = await healthRes.json();
|
|
3394
|
+
var healthDot = document.getElementById('healthDot');
|
|
3395
|
+
var healthLabel = document.getElementById('healthLabel');
|
|
3396
|
+
var healthSummary = document.getElementById('healthSummary');
|
|
3397
|
+
var healthIssues = document.getElementById('healthIssues');
|
|
3398
|
+
var cleanupBtn = document.getElementById('cleanupBtn');
|
|
3399
|
+
var statusMap = { healthy: '✅ Healthy', degraded: '⚠️ Degraded', unhealthy: '🔴 Unhealthy' };
|
|
3400
|
+
healthDot.className = 'health-dot ' + (healthData.status || 'unknown');
|
|
3401
|
+
healthLabel.textContent = statusMap[healthData.status] || '❓ Unknown';
|
|
3402
|
+
var t = healthData.totals || {};
|
|
3403
|
+
healthSummary.textContent = (t.activeEntries || 0) + ' entries · ' + (t.handoffs || 0) + ' handoffs · ' + (t.rollups || 0) + ' rollups' + (t.crdtMerges ? ' · 🔄 ' + t.crdtMerges + ' merges' : '');
|
|
3404
|
+
var issues = healthData.issues || [];
|
|
3405
|
+
if (issues.length > 0) {
|
|
3406
|
+
var sevIcons = { error: '🔴', warning: '🟡', info: '🔵' };
|
|
3407
|
+
healthIssues.innerHTML = issues.map(function(i) {
|
|
3408
|
+
return '<div class="issue-row"><span>' + (sevIcons[i.severity] || '❓') + '</span><span>' + escapeHtml(i.message) + '</span></div>';
|
|
3409
|
+
}).join('');
|
|
3410
|
+
if (cleanupBtn) { cleanupBtn.disabled = false; cleanupBtn.textContent = '🧹 Fix Issues'; cleanupBtn.style.display = 'inline-block'; }
|
|
3411
|
+
} else {
|
|
3412
|
+
healthIssues.innerHTML = '<div style="color:var(--accent-green);font-size:0.8rem">🎉 No issues found</div>';
|
|
3413
|
+
if (cleanupBtn) cleanupBtn.style.display = 'none';
|
|
3414
|
+
}
|
|
3415
|
+
}).catch(function(e) {
|
|
3416
|
+
// Health re-check failed — ensure button is usable
|
|
3417
|
+
if (btn) { btn.disabled = false; btn.textContent = '🧹 Fix Issues';
|
|
3418
|
+
})
|
|
3419
|
+
}
|
|
3420
|
+
}, 400).then(function(res) {
|
|
3421
|
+
|
|
3422
|
+
}).catch(function(e) {
|
|
3423
|
+
finishProgress(false, 'Request failed');
|
|
3424
|
+
showFixedToast('Cleanup request failed.', false);
|
|
3425
|
+
if (btn) { btn.disabled = false; btn.textContent = '🧹 Fix Issues';
|
|
3426
|
+
})
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
function showFixedToast(msg, ok) {
|
|
3431
|
+
var t = document.getElementById('fixedToast');
|
|
3432
|
+
t.textContent = (ok === false ? '❌ ' : '✅ ') + msg;
|
|
3433
|
+
t.classList.add('show');
|
|
3434
|
+
setTimeout(function() { t.classList.remove('show'); }, 3500);
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
// ─── PWA Service Worker Registration ───
|
|
3438
|
+
if ('serviceWorker' in navigator) {
|
|
3439
|
+
window.addEventListener('load', function() {
|
|
3440
|
+
navigator.serviceWorker.register('/sw.js').then(function(reg) {
|
|
3441
|
+
console.log('[Dashboard] Service Worker registered with scope:', reg.scope);
|
|
3442
|
+
|
|
3443
|
+
reg.addEventListener('updatefound', function() {
|
|
3444
|
+
var newWorker = reg.installing;
|
|
3445
|
+
newWorker.addEventListener('statechange', function() {
|
|
3446
|
+
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
|
3447
|
+
var toast = document.createElement('div');
|
|
3448
|
+
toast.style.cssText = 'position:fixed;bottom:2rem;right:2rem;background:var(--bg-glass);backdrop-filter:blur(12px);border:1px solid var(--border-glass);padding:1rem 1.5rem;border-radius:12px;display:flex;align-items:center;gap:1.5rem;z-index:9999;box-shadow:0 10px 30px rgba(0,0,0,0.5);transform:translateY(0);transition:transform 0.3s, opacity 0.3s;';
|
|
3449
|
+
toast.innerHTML = '<div><p style="font-weight:600;margin-bottom:0.25rem;color:var(--text-primary);">Update Available</p><p style="color:var(--text-secondary);font-size:0.85rem;">A new version of Prism is ready.</p></div><button style="background:linear-gradient(135deg, var(--accent-purple), var(--accent-blue));color:white;border:none;padding:0.5rem 1rem;border-radius:6px;cursor:pointer;font-weight:600;">Refresh</button>';
|
|
3450
|
+
|
|
3451
|
+
toast.querySelector('button').addEventListener('click', function() {
|
|
3452
|
+
newWorker.postMessage({ action: 'skipWaiting' });
|
|
3453
|
+
toast.style.opacity = '0';
|
|
3454
|
+
});
|
|
3455
|
+
document.body.appendChild(toast);
|
|
3456
|
+
}
|
|
3457
|
+
});
|
|
3458
|
+
});
|
|
3459
|
+
}).catch(function(err) {
|
|
3460
|
+
console.error('[Dashboard] Service Worker registration failed:', err);
|
|
3461
|
+
});
|
|
3462
|
+
|
|
3463
|
+
var refreshing = false;
|
|
3464
|
+
navigator.serviceWorker.addEventListener('controllerchange', function() {
|
|
3465
|
+
if (!refreshing) {
|
|
3466
|
+
refreshing = true;
|
|
3467
|
+
window.location.reload();
|
|
3468
|
+
}
|
|
3469
|
+
});
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
3472
|
+
</script>
|
|
3473
|
+
</body>
|
|
3474
|
+
</html>`;
|
|
3475
|
+
}
|