mneme-cli 0.4.0__py3-none-any.whl
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.
- mneme/__init__.py +8 -0
- mneme/__main__.py +5 -0
- mneme/config.py +103 -0
- mneme/core.py +6526 -0
- mneme/profiles/eu-mdr.md +196 -0
- mneme/profiles/iso-13485.md +182 -0
- mneme/profiles/mappings/dds.json +21 -0
- mneme/profiles/mappings/requirements.json +22 -0
- mneme/profiles/mappings/risk-register.json +24 -0
- mneme/profiles/mappings/test-cases.json +21 -0
- mneme/profiles/mappings/user-needs.json +19 -0
- mneme/server.py +312 -0
- mneme/templates/workspace/.gitignore +9 -0
- mneme/templates/workspace/AGENTS.md +706 -0
- mneme/templates/workspace/README.md +33 -0
- mneme/templates/workspace/inbox/.gitkeep +0 -0
- mneme/templates/workspace/index.md +18 -0
- mneme/templates/workspace/log.md +6 -0
- mneme/templates/workspace/profiles/README.md +109 -0
- mneme/templates/workspace/profiles/mappings/.gitkeep +0 -0
- mneme/templates/workspace/schema/entities.json +5 -0
- mneme/templates/workspace/schema/graph.json +6 -0
- mneme/templates/workspace/schema/tags.json +5 -0
- mneme/templates/workspace/sources/.gitkeep +0 -0
- mneme/templates/workspace/wiki/_templates/page.md +31 -0
- mneme/ui.html +1520 -0
- mneme_cli-0.4.0.dist-info/METADATA +499 -0
- mneme_cli-0.4.0.dist-info/RECORD +32 -0
- mneme_cli-0.4.0.dist-info/WHEEL +5 -0
- mneme_cli-0.4.0.dist-info/entry_points.txt +2 -0
- mneme_cli-0.4.0.dist-info/licenses/LICENSE +21 -0
- mneme_cli-0.4.0.dist-info/top_level.txt +1 -0
mneme/ui.html
ADDED
|
@@ -0,0 +1,1520 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Mnemosyne</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0d1117;
|
|
12
|
+
--surface: #161b22;
|
|
13
|
+
--border: #21262d;
|
|
14
|
+
--border-hover: #30363d;
|
|
15
|
+
--text: #e6edf3;
|
|
16
|
+
--text-muted: #7d8590;
|
|
17
|
+
--text-dim: #484f58;
|
|
18
|
+
--accent: #58a6ff;
|
|
19
|
+
--accent-dim: #1f6feb33;
|
|
20
|
+
--green: #3fb950;
|
|
21
|
+
--green-dim: #238636;
|
|
22
|
+
--amber: #d29922;
|
|
23
|
+
--amber-dim: #9e6a0333;
|
|
24
|
+
--red: #f85149;
|
|
25
|
+
--red-dim: #da363333;
|
|
26
|
+
--purple: #a371f7;
|
|
27
|
+
--code-bg: #1c2128;
|
|
28
|
+
--radius: 6px;
|
|
29
|
+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
30
|
+
--mono: ui-monospace, "SFMono-Regular", "Cascadia Code", "Fira Code", monospace;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
html, body {
|
|
34
|
+
height: 100%;
|
|
35
|
+
background: var(--bg);
|
|
36
|
+
color: var(--text);
|
|
37
|
+
font-family: var(--font);
|
|
38
|
+
font-size: 14px;
|
|
39
|
+
line-height: 1.6;
|
|
40
|
+
-webkit-font-smoothing: antialiased;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ---- NAV ---- */
|
|
44
|
+
#nav {
|
|
45
|
+
position: sticky;
|
|
46
|
+
top: 0;
|
|
47
|
+
z-index: 100;
|
|
48
|
+
background: var(--surface);
|
|
49
|
+
border-bottom: 1px solid var(--border);
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 0;
|
|
53
|
+
padding: 0 20px;
|
|
54
|
+
height: 48px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#nav-brand {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: 8px;
|
|
61
|
+
margin-right: 28px;
|
|
62
|
+
text-decoration: none;
|
|
63
|
+
color: var(--text);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#nav-brand .logo-icon {
|
|
67
|
+
width: 22px;
|
|
68
|
+
height: 22px;
|
|
69
|
+
background: linear-gradient(135deg, var(--accent) 0%, var(--purple) 100%);
|
|
70
|
+
border-radius: 4px;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
font-size: 11px;
|
|
75
|
+
font-weight: 700;
|
|
76
|
+
color: #fff;
|
|
77
|
+
flex-shrink: 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#nav-brand span {
|
|
81
|
+
font-size: 14px;
|
|
82
|
+
font-weight: 600;
|
|
83
|
+
letter-spacing: -0.01em;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.nav-tabs {
|
|
87
|
+
display: flex;
|
|
88
|
+
gap: 0;
|
|
89
|
+
height: 100%;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.nav-tab {
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
padding: 0 14px;
|
|
96
|
+
height: 100%;
|
|
97
|
+
font-size: 13px;
|
|
98
|
+
font-weight: 500;
|
|
99
|
+
color: var(--text-muted);
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
border-bottom: 2px solid transparent;
|
|
102
|
+
transition: color 0.15s, border-color 0.15s;
|
|
103
|
+
user-select: none;
|
|
104
|
+
white-space: nowrap;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.nav-tab:hover { color: var(--text); }
|
|
108
|
+
|
|
109
|
+
.nav-tab.active {
|
|
110
|
+
color: var(--text);
|
|
111
|
+
border-bottom-color: var(--accent);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.nav-spacer { flex: 1; }
|
|
115
|
+
|
|
116
|
+
#nav-status {
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: 6px;
|
|
120
|
+
font-size: 12px;
|
|
121
|
+
color: var(--text-muted);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.status-dot {
|
|
125
|
+
width: 6px;
|
|
126
|
+
height: 6px;
|
|
127
|
+
border-radius: 50%;
|
|
128
|
+
background: var(--text-dim);
|
|
129
|
+
transition: background 0.3s;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.status-dot.ok { background: var(--green); }
|
|
133
|
+
.status-dot.error { background: var(--red); }
|
|
134
|
+
|
|
135
|
+
/* ---- MAIN ---- */
|
|
136
|
+
#main {
|
|
137
|
+
max-width: 1200px;
|
|
138
|
+
margin: 0 auto;
|
|
139
|
+
padding: 28px 20px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.tab-panel { display: none; }
|
|
143
|
+
.tab-panel.active { display: block; }
|
|
144
|
+
|
|
145
|
+
/* ---- SHARED COMPONENTS ---- */
|
|
146
|
+
.section-header {
|
|
147
|
+
font-size: 12px;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
text-transform: uppercase;
|
|
150
|
+
letter-spacing: 0.08em;
|
|
151
|
+
color: var(--text-muted);
|
|
152
|
+
margin-bottom: 12px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.card {
|
|
156
|
+
background: var(--surface);
|
|
157
|
+
border: 1px solid var(--border);
|
|
158
|
+
border-radius: var(--radius);
|
|
159
|
+
padding: 16px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.badge {
|
|
163
|
+
display: inline-flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
padding: 2px 8px;
|
|
166
|
+
border-radius: 3px;
|
|
167
|
+
font-size: 11px;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
letter-spacing: 0.03em;
|
|
170
|
+
text-transform: uppercase;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.badge-wiki { background: var(--accent-dim); color: var(--accent); }
|
|
174
|
+
.badge-memvid { background: #3d2b5033; color: var(--purple); }
|
|
175
|
+
.badge-client { background: #1f2937; color: #9ca3af; border: 1px solid var(--border); }
|
|
176
|
+
.badge-high { background: #1a3a2a; color: var(--green); }
|
|
177
|
+
.badge-medium { background: var(--amber-dim); color: var(--amber); }
|
|
178
|
+
.badge-low { background: var(--red-dim); color: var(--red); }
|
|
179
|
+
.badge-type { background: var(--code-bg); color: var(--text-muted); border: 1px solid var(--border); }
|
|
180
|
+
|
|
181
|
+
.btn {
|
|
182
|
+
display: inline-flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
gap: 6px;
|
|
185
|
+
padding: 7px 14px;
|
|
186
|
+
border-radius: var(--radius);
|
|
187
|
+
font-size: 13px;
|
|
188
|
+
font-weight: 500;
|
|
189
|
+
font-family: var(--font);
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
transition: background 0.15s, border-color 0.15s, opacity 0.15s;
|
|
192
|
+
border: 1px solid var(--border);
|
|
193
|
+
background: var(--surface);
|
|
194
|
+
color: var(--text);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.btn:hover { background: var(--border); border-color: var(--border-hover); }
|
|
198
|
+
|
|
199
|
+
.btn-primary {
|
|
200
|
+
background: var(--accent);
|
|
201
|
+
border-color: var(--accent);
|
|
202
|
+
color: #0d1117;
|
|
203
|
+
font-weight: 600;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.btn-primary:hover { background: #79bcff; border-color: #79bcff; }
|
|
207
|
+
|
|
208
|
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
209
|
+
|
|
210
|
+
/* ---- DASHBOARD ---- */
|
|
211
|
+
.stats-grid {
|
|
212
|
+
display: grid;
|
|
213
|
+
grid-template-columns: repeat(3, 1fr);
|
|
214
|
+
gap: 12px;
|
|
215
|
+
margin-bottom: 24px;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.stat-card {
|
|
219
|
+
background: var(--surface);
|
|
220
|
+
border: 1px solid var(--border);
|
|
221
|
+
border-radius: var(--radius);
|
|
222
|
+
padding: 18px 20px;
|
|
223
|
+
transition: border-color 0.15s;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.stat-card:hover { border-color: var(--border-hover); }
|
|
227
|
+
|
|
228
|
+
.stat-value {
|
|
229
|
+
font-size: 28px;
|
|
230
|
+
font-weight: 700;
|
|
231
|
+
font-family: var(--mono);
|
|
232
|
+
color: var(--text);
|
|
233
|
+
line-height: 1.1;
|
|
234
|
+
margin-bottom: 4px;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.stat-label {
|
|
238
|
+
font-size: 12px;
|
|
239
|
+
color: var(--text-muted);
|
|
240
|
+
font-weight: 500;
|
|
241
|
+
text-transform: uppercase;
|
|
242
|
+
letter-spacing: 0.05em;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.stat-sub {
|
|
246
|
+
font-size: 11px;
|
|
247
|
+
color: var(--text-dim);
|
|
248
|
+
margin-top: 4px;
|
|
249
|
+
font-family: var(--mono);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.clients-grid {
|
|
253
|
+
display: grid;
|
|
254
|
+
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
255
|
+
gap: 12px;
|
|
256
|
+
margin-bottom: 24px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.client-card {
|
|
260
|
+
background: var(--surface);
|
|
261
|
+
border: 1px solid var(--border);
|
|
262
|
+
border-radius: var(--radius);
|
|
263
|
+
padding: 16px;
|
|
264
|
+
transition: border-color 0.15s;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.client-card:hover { border-color: var(--border-hover); }
|
|
268
|
+
|
|
269
|
+
.client-card-name {
|
|
270
|
+
font-weight: 600;
|
|
271
|
+
font-size: 14px;
|
|
272
|
+
color: var(--text);
|
|
273
|
+
margin-bottom: 8px;
|
|
274
|
+
display: flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
gap: 8px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.client-card-name .dot {
|
|
280
|
+
width: 8px;
|
|
281
|
+
height: 8px;
|
|
282
|
+
border-radius: 50%;
|
|
283
|
+
background: var(--accent);
|
|
284
|
+
flex-shrink: 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.client-card-meta {
|
|
288
|
+
display: flex;
|
|
289
|
+
flex-direction: column;
|
|
290
|
+
gap: 4px;
|
|
291
|
+
font-size: 12px;
|
|
292
|
+
color: var(--text-muted);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.client-card-meta span { font-family: var(--mono); color: var(--text-dim); }
|
|
296
|
+
|
|
297
|
+
.activity-log {
|
|
298
|
+
background: var(--surface);
|
|
299
|
+
border: 1px solid var(--border);
|
|
300
|
+
border-radius: var(--radius);
|
|
301
|
+
overflow: hidden;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.activity-header {
|
|
305
|
+
padding: 12px 16px;
|
|
306
|
+
border-bottom: 1px solid var(--border);
|
|
307
|
+
font-size: 12px;
|
|
308
|
+
font-weight: 600;
|
|
309
|
+
text-transform: uppercase;
|
|
310
|
+
letter-spacing: 0.08em;
|
|
311
|
+
color: var(--text-muted);
|
|
312
|
+
display: flex;
|
|
313
|
+
align-items: center;
|
|
314
|
+
justify-content: space-between;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.activity-entry {
|
|
318
|
+
padding: 10px 16px;
|
|
319
|
+
border-bottom: 1px solid var(--border);
|
|
320
|
+
display: grid;
|
|
321
|
+
grid-template-columns: 140px 80px 1fr;
|
|
322
|
+
gap: 12px;
|
|
323
|
+
align-items: center;
|
|
324
|
+
font-size: 12px;
|
|
325
|
+
transition: background 0.1s;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.activity-entry:last-child { border-bottom: none; }
|
|
329
|
+
.activity-entry:hover { background: rgba(255,255,255,0.02); }
|
|
330
|
+
|
|
331
|
+
.activity-date { color: var(--text-dim); font-family: var(--mono); }
|
|
332
|
+
|
|
333
|
+
.activity-op {
|
|
334
|
+
font-weight: 600;
|
|
335
|
+
font-size: 11px;
|
|
336
|
+
text-transform: uppercase;
|
|
337
|
+
letter-spacing: 0.05em;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.activity-op.ingest { color: var(--green); }
|
|
341
|
+
.activity-op.query { color: var(--accent); }
|
|
342
|
+
.activity-op.init { color: var(--purple); }
|
|
343
|
+
.activity-op.lint { color: var(--amber); }
|
|
344
|
+
.activity-op.update { color: var(--text-muted); }
|
|
345
|
+
|
|
346
|
+
.activity-desc { color: var(--text-muted); line-height: 1.4; }
|
|
347
|
+
|
|
348
|
+
/* ---- SEARCH ---- */
|
|
349
|
+
.search-bar {
|
|
350
|
+
display: flex;
|
|
351
|
+
gap: 10px;
|
|
352
|
+
margin-bottom: 16px;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.search-input {
|
|
356
|
+
flex: 1;
|
|
357
|
+
padding: 10px 14px;
|
|
358
|
+
background: var(--surface);
|
|
359
|
+
border: 1px solid var(--border);
|
|
360
|
+
border-radius: var(--radius);
|
|
361
|
+
color: var(--text);
|
|
362
|
+
font-size: 14px;
|
|
363
|
+
font-family: var(--font);
|
|
364
|
+
outline: none;
|
|
365
|
+
transition: border-color 0.15s;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.search-input:focus { border-color: var(--accent); }
|
|
369
|
+
.search-input::placeholder { color: var(--text-dim); }
|
|
370
|
+
|
|
371
|
+
.search-meta {
|
|
372
|
+
font-size: 12px;
|
|
373
|
+
color: var(--text-muted);
|
|
374
|
+
margin-bottom: 14px;
|
|
375
|
+
min-height: 20px;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.result-card {
|
|
379
|
+
background: var(--surface);
|
|
380
|
+
border: 1px solid var(--border);
|
|
381
|
+
border-radius: var(--radius);
|
|
382
|
+
padding: 14px 16px;
|
|
383
|
+
margin-bottom: 8px;
|
|
384
|
+
cursor: pointer;
|
|
385
|
+
transition: border-color 0.15s, background 0.1s;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.result-card:hover { border-color: var(--border-hover); background: rgba(88,166,255,0.03); }
|
|
389
|
+
|
|
390
|
+
.result-header {
|
|
391
|
+
display: flex;
|
|
392
|
+
align-items: center;
|
|
393
|
+
gap: 8px;
|
|
394
|
+
margin-bottom: 6px;
|
|
395
|
+
flex-wrap: wrap;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.result-score {
|
|
399
|
+
margin-left: auto;
|
|
400
|
+
font-size: 11px;
|
|
401
|
+
color: var(--text-dim);
|
|
402
|
+
font-family: var(--mono);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.result-title {
|
|
406
|
+
font-weight: 600;
|
|
407
|
+
font-size: 14px;
|
|
408
|
+
color: var(--accent);
|
|
409
|
+
margin-bottom: 6px;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.result-snippet {
|
|
413
|
+
font-size: 13px;
|
|
414
|
+
color: var(--text-muted);
|
|
415
|
+
line-height: 1.6;
|
|
416
|
+
font-family: var(--mono);
|
|
417
|
+
white-space: pre-wrap;
|
|
418
|
+
overflow: hidden;
|
|
419
|
+
display: -webkit-box;
|
|
420
|
+
-webkit-line-clamp: 3;
|
|
421
|
+
-webkit-box-orient: vertical;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.result-path {
|
|
425
|
+
font-size: 11px;
|
|
426
|
+
color: var(--text-dim);
|
|
427
|
+
font-family: var(--mono);
|
|
428
|
+
margin-top: 6px;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* ---- WIKI ---- */
|
|
432
|
+
.wiki-layout {
|
|
433
|
+
display: grid;
|
|
434
|
+
grid-template-columns: 240px 1fr;
|
|
435
|
+
gap: 20px;
|
|
436
|
+
min-height: calc(100vh - 100px);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.wiki-sidebar {
|
|
440
|
+
background: var(--surface);
|
|
441
|
+
border: 1px solid var(--border);
|
|
442
|
+
border-radius: var(--radius);
|
|
443
|
+
padding: 12px 0;
|
|
444
|
+
height: fit-content;
|
|
445
|
+
position: sticky;
|
|
446
|
+
top: 68px;
|
|
447
|
+
max-height: calc(100vh - 80px);
|
|
448
|
+
overflow-y: auto;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.sidebar-title {
|
|
452
|
+
padding: 0 14px 8px;
|
|
453
|
+
font-size: 11px;
|
|
454
|
+
font-weight: 600;
|
|
455
|
+
text-transform: uppercase;
|
|
456
|
+
letter-spacing: 0.08em;
|
|
457
|
+
color: var(--text-dim);
|
|
458
|
+
border-bottom: 1px solid var(--border);
|
|
459
|
+
margin-bottom: 8px;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.client-group { margin-bottom: 4px; }
|
|
463
|
+
|
|
464
|
+
.client-group-name {
|
|
465
|
+
display: flex;
|
|
466
|
+
align-items: center;
|
|
467
|
+
gap: 6px;
|
|
468
|
+
padding: 6px 14px;
|
|
469
|
+
font-size: 12px;
|
|
470
|
+
font-weight: 600;
|
|
471
|
+
color: var(--text-muted);
|
|
472
|
+
cursor: pointer;
|
|
473
|
+
user-select: none;
|
|
474
|
+
transition: color 0.1s, background 0.1s;
|
|
475
|
+
border-radius: 3px;
|
|
476
|
+
margin: 0 4px;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.client-group-name:hover { color: var(--text); background: rgba(255,255,255,0.04); }
|
|
480
|
+
|
|
481
|
+
.client-group-name .toggle { font-size: 10px; color: var(--text-dim); transition: transform 0.15s; }
|
|
482
|
+
.client-group-name.open .toggle { transform: rotate(90deg); }
|
|
483
|
+
|
|
484
|
+
.client-pages { display: none; }
|
|
485
|
+
.client-pages.open { display: block; }
|
|
486
|
+
|
|
487
|
+
.wiki-page-link {
|
|
488
|
+
display: block;
|
|
489
|
+
padding: 5px 14px 5px 30px;
|
|
490
|
+
font-size: 12px;
|
|
491
|
+
color: var(--text-muted);
|
|
492
|
+
cursor: pointer;
|
|
493
|
+
transition: color 0.1s, background 0.1s;
|
|
494
|
+
border-radius: 3px;
|
|
495
|
+
margin: 1px 4px;
|
|
496
|
+
text-overflow: ellipsis;
|
|
497
|
+
overflow: hidden;
|
|
498
|
+
white-space: nowrap;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.wiki-page-link:hover { color: var(--text); background: rgba(255,255,255,0.04); }
|
|
502
|
+
.wiki-page-link.active { color: var(--accent); background: var(--accent-dim); }
|
|
503
|
+
|
|
504
|
+
.wiki-content {
|
|
505
|
+
background: var(--surface);
|
|
506
|
+
border: 1px solid var(--border);
|
|
507
|
+
border-radius: var(--radius);
|
|
508
|
+
padding: 28px 32px;
|
|
509
|
+
min-height: 400px;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.page-meta {
|
|
513
|
+
display: flex;
|
|
514
|
+
flex-wrap: wrap;
|
|
515
|
+
gap: 8px;
|
|
516
|
+
margin-bottom: 24px;
|
|
517
|
+
padding-bottom: 16px;
|
|
518
|
+
border-bottom: 1px solid var(--border);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.wiki-placeholder {
|
|
522
|
+
text-align: center;
|
|
523
|
+
padding: 60px 20px;
|
|
524
|
+
color: var(--text-dim);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.wiki-placeholder .icon {
|
|
528
|
+
font-size: 40px;
|
|
529
|
+
margin-bottom: 12px;
|
|
530
|
+
opacity: 0.3;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/* Markdown rendered content */
|
|
534
|
+
#page-body h1 { font-size: 22px; font-weight: 700; margin: 0 0 16px; color: var(--text); line-height: 1.3; }
|
|
535
|
+
#page-body h2 { font-size: 17px; font-weight: 600; margin: 28px 0 12px; color: var(--text); padding-bottom: 6px; border-bottom: 1px solid var(--border); }
|
|
536
|
+
#page-body h3 { font-size: 15px; font-weight: 600; margin: 20px 0 8px; color: var(--text-muted); }
|
|
537
|
+
#page-body p { margin-bottom: 14px; color: var(--text-muted); line-height: 1.7; }
|
|
538
|
+
#page-body ul, #page-body ol { margin: 0 0 14px 20px; color: var(--text-muted); }
|
|
539
|
+
#page-body li { margin-bottom: 4px; line-height: 1.6; }
|
|
540
|
+
#page-body strong { color: var(--text); font-weight: 600; }
|
|
541
|
+
#page-body em { color: var(--text-muted); font-style: italic; }
|
|
542
|
+
#page-body code { font-family: var(--mono); font-size: 12px; background: var(--code-bg); padding: 2px 5px; border-radius: 3px; color: #ff8c00; }
|
|
543
|
+
#page-body pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; overflow-x: auto; margin-bottom: 16px; }
|
|
544
|
+
#page-body pre code { background: none; padding: 0; color: var(--text-muted); font-size: 13px; }
|
|
545
|
+
#page-body table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-size: 13px; }
|
|
546
|
+
#page-body th { background: var(--code-bg); color: var(--text); font-weight: 600; padding: 8px 12px; text-align: left; border: 1px solid var(--border); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
547
|
+
#page-body td { padding: 8px 12px; border: 1px solid var(--border); color: var(--text-muted); vertical-align: top; }
|
|
548
|
+
#page-body tr:nth-child(even) td { background: rgba(255,255,255,0.02); }
|
|
549
|
+
#page-body a.wikilink { color: var(--accent); text-decoration: none; border-bottom: 1px dashed var(--accent-dim); cursor: pointer; }
|
|
550
|
+
#page-body a.wikilink:hover { border-bottom-color: var(--accent); }
|
|
551
|
+
#page-body blockquote { border-left: 3px solid var(--border-hover); margin: 0 0 14px; padding: 8px 16px; color: var(--text-muted); background: rgba(255,255,255,0.02); }
|
|
552
|
+
#page-body hr { border: none; border-top: 1px solid var(--border); margin: 24px 0; }
|
|
553
|
+
|
|
554
|
+
/* ---- ENTITIES ---- */
|
|
555
|
+
.entities-controls {
|
|
556
|
+
display: flex;
|
|
557
|
+
gap: 10px;
|
|
558
|
+
margin-bottom: 16px;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.entities-controls input,
|
|
562
|
+
.entities-controls select {
|
|
563
|
+
padding: 8px 12px;
|
|
564
|
+
background: var(--surface);
|
|
565
|
+
border: 1px solid var(--border);
|
|
566
|
+
border-radius: var(--radius);
|
|
567
|
+
color: var(--text);
|
|
568
|
+
font-size: 13px;
|
|
569
|
+
font-family: var(--font);
|
|
570
|
+
outline: none;
|
|
571
|
+
transition: border-color 0.15s;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.entities-controls input { flex: 1; }
|
|
575
|
+
.entities-controls input:focus,
|
|
576
|
+
.entities-controls select:focus { border-color: var(--accent); }
|
|
577
|
+
|
|
578
|
+
.entities-controls select option { background: var(--surface); }
|
|
579
|
+
|
|
580
|
+
.entities-table {
|
|
581
|
+
width: 100%;
|
|
582
|
+
border-collapse: collapse;
|
|
583
|
+
font-size: 13px;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.entities-table th {
|
|
587
|
+
background: var(--surface);
|
|
588
|
+
color: var(--text-muted);
|
|
589
|
+
font-weight: 600;
|
|
590
|
+
font-size: 11px;
|
|
591
|
+
text-transform: uppercase;
|
|
592
|
+
letter-spacing: 0.06em;
|
|
593
|
+
padding: 10px 14px;
|
|
594
|
+
text-align: left;
|
|
595
|
+
border: 1px solid var(--border);
|
|
596
|
+
position: sticky;
|
|
597
|
+
top: 48px;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.entities-table td {
|
|
601
|
+
padding: 9px 14px;
|
|
602
|
+
border: 1px solid var(--border);
|
|
603
|
+
color: var(--text-muted);
|
|
604
|
+
vertical-align: middle;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.entities-table tbody tr:hover td { background: rgba(255,255,255,0.02); }
|
|
608
|
+
.entities-table tbody tr:hover td:first-child { color: var(--accent); }
|
|
609
|
+
|
|
610
|
+
.entity-name { font-weight: 500; color: var(--text); cursor: pointer; }
|
|
611
|
+
.entity-name:hover { color: var(--accent); }
|
|
612
|
+
|
|
613
|
+
.entity-link {
|
|
614
|
+
color: var(--accent);
|
|
615
|
+
cursor: pointer;
|
|
616
|
+
font-family: var(--mono);
|
|
617
|
+
font-size: 12px;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.entity-link:hover { text-decoration: underline; }
|
|
621
|
+
|
|
622
|
+
.entity-type {
|
|
623
|
+
font-family: var(--mono);
|
|
624
|
+
font-size: 11px;
|
|
625
|
+
color: var(--text-dim);
|
|
626
|
+
background: var(--code-bg);
|
|
627
|
+
padding: 2px 6px;
|
|
628
|
+
border-radius: 3px;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/* ---- HEALTH ---- */
|
|
632
|
+
.health-actions {
|
|
633
|
+
display: flex;
|
|
634
|
+
gap: 10px;
|
|
635
|
+
margin-bottom: 24px;
|
|
636
|
+
align-items: center;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.health-status-bar {
|
|
640
|
+
margin-bottom: 24px;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.health-score {
|
|
644
|
+
display: flex;
|
|
645
|
+
align-items: center;
|
|
646
|
+
gap: 16px;
|
|
647
|
+
margin-bottom: 16px;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.health-score-value {
|
|
651
|
+
font-size: 48px;
|
|
652
|
+
font-weight: 700;
|
|
653
|
+
font-family: var(--mono);
|
|
654
|
+
line-height: 1;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.health-score-label { font-size: 13px; color: var(--text-muted); }
|
|
658
|
+
|
|
659
|
+
.progress-bar {
|
|
660
|
+
width: 100%;
|
|
661
|
+
height: 8px;
|
|
662
|
+
background: var(--border);
|
|
663
|
+
border-radius: 4px;
|
|
664
|
+
overflow: hidden;
|
|
665
|
+
margin-bottom: 6px;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.progress-fill {
|
|
669
|
+
height: 100%;
|
|
670
|
+
border-radius: 4px;
|
|
671
|
+
transition: width 0.6s ease;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.progress-fill.green { background: var(--green); }
|
|
675
|
+
.progress-fill.amber { background: var(--amber); }
|
|
676
|
+
.progress-fill.red { background: var(--red); }
|
|
677
|
+
|
|
678
|
+
.health-section {
|
|
679
|
+
margin-bottom: 20px;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.health-section-title {
|
|
683
|
+
font-size: 12px;
|
|
684
|
+
font-weight: 600;
|
|
685
|
+
text-transform: uppercase;
|
|
686
|
+
letter-spacing: 0.08em;
|
|
687
|
+
color: var(--text-muted);
|
|
688
|
+
margin-bottom: 10px;
|
|
689
|
+
display: flex;
|
|
690
|
+
align-items: center;
|
|
691
|
+
gap: 8px;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.health-count {
|
|
695
|
+
background: var(--code-bg);
|
|
696
|
+
color: var(--text-dim);
|
|
697
|
+
font-family: var(--mono);
|
|
698
|
+
font-size: 11px;
|
|
699
|
+
padding: 1px 6px;
|
|
700
|
+
border-radius: 10px;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.health-list {
|
|
704
|
+
background: var(--surface);
|
|
705
|
+
border: 1px solid var(--border);
|
|
706
|
+
border-radius: var(--radius);
|
|
707
|
+
overflow: hidden;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.health-list-item {
|
|
711
|
+
padding: 9px 14px;
|
|
712
|
+
border-bottom: 1px solid var(--border);
|
|
713
|
+
font-size: 12px;
|
|
714
|
+
font-family: var(--mono);
|
|
715
|
+
color: var(--text-muted);
|
|
716
|
+
display: flex;
|
|
717
|
+
align-items: center;
|
|
718
|
+
gap: 10px;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.health-list-item:last-child { border-bottom: none; }
|
|
722
|
+
|
|
723
|
+
.health-list-item .icon { font-size: 14px; flex-shrink: 0; }
|
|
724
|
+
|
|
725
|
+
.health-empty {
|
|
726
|
+
padding: 14px;
|
|
727
|
+
text-align: center;
|
|
728
|
+
color: var(--text-dim);
|
|
729
|
+
font-size: 12px;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/* ---- LOADING / EMPTY STATES ---- */
|
|
733
|
+
.loading {
|
|
734
|
+
color: var(--text-dim);
|
|
735
|
+
font-size: 13px;
|
|
736
|
+
padding: 20px 0;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.empty-state {
|
|
740
|
+
text-align: center;
|
|
741
|
+
padding: 40px 20px;
|
|
742
|
+
color: var(--text-dim);
|
|
743
|
+
font-size: 13px;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/* ---- SCROLLBAR ---- */
|
|
747
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
748
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
749
|
+
::-webkit-scrollbar-thumb { background: var(--border-hover); border-radius: 3px; }
|
|
750
|
+
::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
|
|
751
|
+
|
|
752
|
+
/* ---- RESPONSIVE ---- */
|
|
753
|
+
@media (max-width: 900px) {
|
|
754
|
+
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
755
|
+
.wiki-layout { grid-template-columns: 1fr; }
|
|
756
|
+
.wiki-sidebar { position: static; max-height: 300px; }
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
@media (max-width: 600px) {
|
|
760
|
+
.stats-grid { grid-template-columns: 1fr; }
|
|
761
|
+
#nav { padding: 0 12px; }
|
|
762
|
+
#main { padding: 16px 12px; }
|
|
763
|
+
.nav-tab { padding: 0 10px; font-size: 12px; }
|
|
764
|
+
.activity-entry { grid-template-columns: 1fr; gap: 2px; }
|
|
765
|
+
}
|
|
766
|
+
</style>
|
|
767
|
+
</head>
|
|
768
|
+
<body>
|
|
769
|
+
|
|
770
|
+
<nav id="nav">
|
|
771
|
+
<a id="nav-brand" href="#" onclick="return false;">
|
|
772
|
+
<div class="logo-icon">MN</div>
|
|
773
|
+
<span>Mnemosyne</span>
|
|
774
|
+
</a>
|
|
775
|
+
<div class="nav-tabs">
|
|
776
|
+
<div class="nav-tab active" data-tab="dashboard" onclick="switchTab('dashboard')">Dashboard</div>
|
|
777
|
+
<div class="nav-tab" data-tab="search" onclick="switchTab('search')">Search</div>
|
|
778
|
+
<div class="nav-tab" data-tab="wiki" onclick="switchTab('wiki')">Wiki</div>
|
|
779
|
+
<div class="nav-tab" data-tab="entities" onclick="switchTab('entities')">Entities</div>
|
|
780
|
+
<div class="nav-tab" data-tab="health" onclick="switchTab('health')">Health</div>
|
|
781
|
+
</div>
|
|
782
|
+
<div class="nav-spacer"></div>
|
|
783
|
+
<div id="nav-status">
|
|
784
|
+
<div class="status-dot" id="status-dot"></div>
|
|
785
|
+
<span id="status-text">Connecting...</span>
|
|
786
|
+
</div>
|
|
787
|
+
</nav>
|
|
788
|
+
|
|
789
|
+
<div id="main">
|
|
790
|
+
|
|
791
|
+
<!-- DASHBOARD -->
|
|
792
|
+
<div class="tab-panel active" id="tab-dashboard">
|
|
793
|
+
<div class="stats-grid" id="stats-grid">
|
|
794
|
+
<div class="stat-card"><div class="stat-value" id="s-wiki">-</div><div class="stat-label">Wiki Pages</div></div>
|
|
795
|
+
<div class="stat-card"><div class="stat-value" id="s-frames">-</div><div class="stat-label">Memvid Frames</div></div>
|
|
796
|
+
<div class="stat-card"><div class="stat-value" id="s-entities">-</div><div class="stat-label">Entities</div></div>
|
|
797
|
+
<div class="stat-card"><div class="stat-value" id="s-tags">-</div><div class="stat-label">Tags</div></div>
|
|
798
|
+
<div class="stat-card"><div class="stat-value" id="s-latency">-</div><div class="stat-label">Search Latency</div><div class="stat-sub" id="s-latency-sub"></div></div>
|
|
799
|
+
<div class="stat-card"><div class="stat-value" id="s-drift">-</div><div class="stat-label">Sync Status</div><div class="stat-sub" id="s-drift-sub"></div></div>
|
|
800
|
+
</div>
|
|
801
|
+
|
|
802
|
+
<p class="section-header" style="margin-bottom:12px">Clients</p>
|
|
803
|
+
<div class="clients-grid" id="clients-grid">
|
|
804
|
+
<div class="loading">Loading client data...</div>
|
|
805
|
+
</div>
|
|
806
|
+
|
|
807
|
+
<div class="activity-log">
|
|
808
|
+
<div class="activity-header">
|
|
809
|
+
<span>Recent Activity</span>
|
|
810
|
+
<span id="activity-count" style="color:var(--text-dim);font-weight:400;font-size:11px"></span>
|
|
811
|
+
</div>
|
|
812
|
+
<div id="activity-body"><div class="loading" style="padding:16px">Loading log...</div></div>
|
|
813
|
+
</div>
|
|
814
|
+
</div>
|
|
815
|
+
|
|
816
|
+
<!-- SEARCH -->
|
|
817
|
+
<div class="tab-panel" id="tab-search">
|
|
818
|
+
<div class="search-bar">
|
|
819
|
+
<input
|
|
820
|
+
type="text"
|
|
821
|
+
id="search-input"
|
|
822
|
+
class="search-input"
|
|
823
|
+
placeholder="Search across all knowledge..."
|
|
824
|
+
onkeydown="if(event.key==='Enter')doSearch()"
|
|
825
|
+
>
|
|
826
|
+
<button class="btn btn-primary" onclick="doSearch()">Search</button>
|
|
827
|
+
</div>
|
|
828
|
+
<div class="search-meta" id="search-meta"></div>
|
|
829
|
+
<div id="search-results"></div>
|
|
830
|
+
</div>
|
|
831
|
+
|
|
832
|
+
<!-- WIKI -->
|
|
833
|
+
<div class="tab-panel" id="tab-wiki">
|
|
834
|
+
<div class="wiki-layout">
|
|
835
|
+
<div class="wiki-sidebar">
|
|
836
|
+
<div class="sidebar-title">Pages</div>
|
|
837
|
+
<div id="wiki-tree"><div class="loading" style="padding:12px">Loading...</div></div>
|
|
838
|
+
</div>
|
|
839
|
+
<div class="wiki-content" id="wiki-content">
|
|
840
|
+
<div class="wiki-placeholder">
|
|
841
|
+
<div class="icon">📄</div>
|
|
842
|
+
<div style="font-size:14px;color:var(--text-muted);margin-bottom:6px">Select a page from the sidebar</div>
|
|
843
|
+
<div style="font-size:12px">Browse by client or use Search to find specific pages</div>
|
|
844
|
+
</div>
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
</div>
|
|
848
|
+
|
|
849
|
+
<!-- ENTITIES -->
|
|
850
|
+
<div class="tab-panel" id="tab-entities">
|
|
851
|
+
<div class="entities-controls">
|
|
852
|
+
<input type="text" id="entity-filter" placeholder="Filter by name..." oninput="filterEntities()">
|
|
853
|
+
<select id="entity-client-filter" onchange="filterEntities()">
|
|
854
|
+
<option value="">All clients</option>
|
|
855
|
+
</select>
|
|
856
|
+
</div>
|
|
857
|
+
<div id="entities-count" style="font-size:12px;color:var(--text-muted);margin-bottom:12px"></div>
|
|
858
|
+
<table class="entities-table">
|
|
859
|
+
<thead>
|
|
860
|
+
<tr>
|
|
861
|
+
<th>Name</th>
|
|
862
|
+
<th>Client</th>
|
|
863
|
+
<th>Type</th>
|
|
864
|
+
<th>Wiki Page</th>
|
|
865
|
+
</tr>
|
|
866
|
+
</thead>
|
|
867
|
+
<tbody id="entities-body">
|
|
868
|
+
<tr><td colspan="4" class="loading" style="text-align:center;padding:20px">Loading entities...</td></tr>
|
|
869
|
+
</tbody>
|
|
870
|
+
</table>
|
|
871
|
+
</div>
|
|
872
|
+
|
|
873
|
+
<!-- HEALTH -->
|
|
874
|
+
<div class="tab-panel" id="tab-health">
|
|
875
|
+
<div class="health-actions">
|
|
876
|
+
<button class="btn btn-primary" onclick="runHealthCheck()" id="health-btn">Run Health Check</button>
|
|
877
|
+
<button class="btn" onclick="runSync()" id="sync-btn">Sync All Pages</button>
|
|
878
|
+
<span id="health-run-status" style="font-size:12px;color:var(--text-muted)"></span>
|
|
879
|
+
</div>
|
|
880
|
+
<div id="health-results">
|
|
881
|
+
<div class="empty-state">Click "Run Health Check" to analyze sync status, orphan frames, and stale pages.</div>
|
|
882
|
+
</div>
|
|
883
|
+
</div>
|
|
884
|
+
|
|
885
|
+
</div>
|
|
886
|
+
|
|
887
|
+
<script>
|
|
888
|
+
// ---- STATE ----
|
|
889
|
+
const API = ''; // same-origin
|
|
890
|
+
let allEntities = [];
|
|
891
|
+
let wikiTree = {};
|
|
892
|
+
let currentPage = null;
|
|
893
|
+
|
|
894
|
+
// ---- TAB SWITCHING ----
|
|
895
|
+
function switchTab(name) {
|
|
896
|
+
document.querySelectorAll('.nav-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
|
|
897
|
+
document.querySelectorAll('.tab-panel').forEach(p => p.classList.toggle('active', p.id === 'tab-' + name));
|
|
898
|
+
|
|
899
|
+
if (name === 'search') {
|
|
900
|
+
setTimeout(() => document.getElementById('search-input').focus(), 50);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ---- API HELPERS ----
|
|
905
|
+
async function apiFetch(path) {
|
|
906
|
+
const res = await fetch(API + path);
|
|
907
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
908
|
+
return res.json();
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async function apiPost(path) {
|
|
912
|
+
const res = await fetch(API + path, { method: 'POST' });
|
|
913
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
914
|
+
return res.json();
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function setStatus(ok, text) {
|
|
918
|
+
document.getElementById('status-dot').className = 'status-dot ' + (ok ? 'ok' : 'error');
|
|
919
|
+
document.getElementById('status-text').textContent = text;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// ---- FORMAT HELPERS ----
|
|
923
|
+
function fmtBytes(b) {
|
|
924
|
+
if (!b || b === 0) return '0 B';
|
|
925
|
+
const units = ['B','KB','MB','GB'];
|
|
926
|
+
let i = 0;
|
|
927
|
+
while (b >= 1024 && i < units.length - 1) { b /= 1024; i++; }
|
|
928
|
+
return b.toFixed(1) + ' ' + units[i];
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function fmtNum(n) {
|
|
932
|
+
if (n === undefined || n === null || n === '-') return '-';
|
|
933
|
+
return Number(n).toLocaleString();
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function slugToTitle(s) {
|
|
937
|
+
return s.replace(/-/g, ' ').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// ---- DASHBOARD ----
|
|
941
|
+
async function loadDashboard() {
|
|
942
|
+
try {
|
|
943
|
+
const data = await apiFetch('/api/stats');
|
|
944
|
+
setStatus(true, 'Connected');
|
|
945
|
+
|
|
946
|
+
// Wiki stats
|
|
947
|
+
document.getElementById('s-wiki').textContent = fmtNum(data.wiki?.total_pages ?? '-');
|
|
948
|
+
|
|
949
|
+
// Memvid frames
|
|
950
|
+
const frames = data.memvid?.frame_count ?? '-';
|
|
951
|
+
document.getElementById('s-frames').textContent = fmtNum(frames);
|
|
952
|
+
|
|
953
|
+
// Entities + tags
|
|
954
|
+
document.getElementById('s-entities').textContent = fmtNum(data.schema?.entity_count ?? '-');
|
|
955
|
+
document.getElementById('s-tags').textContent = fmtNum(data.schema?.tag_count ?? '-');
|
|
956
|
+
|
|
957
|
+
// Latency
|
|
958
|
+
const lat = data.memvid?.search_latency_ms;
|
|
959
|
+
document.getElementById('s-latency').textContent = lat != null ? lat + 'ms' : '-';
|
|
960
|
+
document.getElementById('s-latency-sub').textContent = lat != null ? 'memvid master' : 'memvid offline';
|
|
961
|
+
|
|
962
|
+
// Drift
|
|
963
|
+
const drift = data.drift?.sync_status ?? '-';
|
|
964
|
+
document.getElementById('s-drift').textContent = typeof drift === 'string' ? drift.split(' ')[0] : '-';
|
|
965
|
+
document.getElementById('s-drift-sub').textContent = typeof drift === 'string' ? drift : drift;
|
|
966
|
+
|
|
967
|
+
// Client cards
|
|
968
|
+
const byClient = data.wiki?.by_client ?? {};
|
|
969
|
+
const archiveSizes = data.memvid?.per_client_archive_sizes ?? {};
|
|
970
|
+
renderClientCards(byClient, archiveSizes);
|
|
971
|
+
|
|
972
|
+
} catch(e) {
|
|
973
|
+
setStatus(false, 'Error');
|
|
974
|
+
document.getElementById('s-wiki').textContent = 'Err';
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Load log separately
|
|
978
|
+
loadActivityLog();
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function renderClientCards(byClient, archiveSizes) {
|
|
982
|
+
const grid = document.getElementById('clients-grid');
|
|
983
|
+
const clients = Object.keys(byClient).filter(c => c !== '_templates');
|
|
984
|
+
if (!clients.length) {
|
|
985
|
+
grid.innerHTML = '<div class="empty-state">No clients found</div>';
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const colors = ['#58a6ff','#3fb950','#a371f7','#d29922','#ff7b72','#39d353'];
|
|
990
|
+
grid.innerHTML = clients.map((c, i) => {
|
|
991
|
+
const pages = byClient[c];
|
|
992
|
+
const archSize = archiveSizes[c];
|
|
993
|
+
const color = colors[i % colors.length];
|
|
994
|
+
return `
|
|
995
|
+
<div class="client-card" onclick="goToWikiClient('${c}')">
|
|
996
|
+
<div class="client-card-name">
|
|
997
|
+
<div class="dot" style="background:${color}"></div>
|
|
998
|
+
${slugToTitle(c)}
|
|
999
|
+
</div>
|
|
1000
|
+
<div class="client-card-meta">
|
|
1001
|
+
<div>${pages} wiki page${pages !== 1 ? 's' : ''}</div>
|
|
1002
|
+
${archSize ? '<div>Archive: <span>' + fmtBytes(archSize) + '</span></div>' : '<div>No archive yet</div>'}
|
|
1003
|
+
</div>
|
|
1004
|
+
</div>`;
|
|
1005
|
+
}).join('');
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function goToWikiClient(client) {
|
|
1009
|
+
switchTab('wiki');
|
|
1010
|
+
setTimeout(() => {
|
|
1011
|
+
const el = document.getElementById('pages-' + client);
|
|
1012
|
+
if (el) {
|
|
1013
|
+
el.classList.add('open');
|
|
1014
|
+
el.previousElementSibling.classList.add('open');
|
|
1015
|
+
}
|
|
1016
|
+
}, 100);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async function loadActivityLog() {
|
|
1020
|
+
const body = document.getElementById('activity-body');
|
|
1021
|
+
try {
|
|
1022
|
+
const data = await apiFetch('/api/log');
|
|
1023
|
+
const lines = (data.content || '').split('\n');
|
|
1024
|
+
const entries = [];
|
|
1025
|
+
|
|
1026
|
+
let current = null;
|
|
1027
|
+
for (const line of lines) {
|
|
1028
|
+
const m = line.match(/^## \[(\d{4}-\d{2}-\d{2}[^\]]*)\]\s+(\w+)\s*\|\s*(.*)/);
|
|
1029
|
+
if (m) {
|
|
1030
|
+
if (current) entries.push(current);
|
|
1031
|
+
current = { date: m[1].trim(), op: m[2].trim(), desc: m[3].trim(), details: [] };
|
|
1032
|
+
} else if (current && line.startsWith('- ')) {
|
|
1033
|
+
current.details.push(line.slice(2).trim());
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (current) entries.push(current);
|
|
1037
|
+
|
|
1038
|
+
document.getElementById('activity-count').textContent = entries.length + ' entries';
|
|
1039
|
+
|
|
1040
|
+
if (!entries.length) {
|
|
1041
|
+
body.innerHTML = '<div class="empty-state">No activity logged yet</div>';
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const shown = entries.slice(0, 20);
|
|
1046
|
+
body.innerHTML = shown.map(e => `
|
|
1047
|
+
<div class="activity-entry">
|
|
1048
|
+
<div class="activity-date">${e.date}</div>
|
|
1049
|
+
<div class="activity-op ${e.op.toLowerCase()}">${e.op}</div>
|
|
1050
|
+
<div class="activity-desc">${escHtml(e.desc)}${e.details.length ? '<br><span style="font-size:11px;color:var(--text-dim)">' + escHtml(e.details[0]) + '</span>' : ''}</div>
|
|
1051
|
+
</div>`).join('');
|
|
1052
|
+
} catch(e) {
|
|
1053
|
+
body.innerHTML = '<div class="empty-state">Could not load log: ' + escHtml(e.message) + '</div>';
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// ---- SEARCH ----
|
|
1058
|
+
async function doSearch() {
|
|
1059
|
+
const q = document.getElementById('search-input').value.trim();
|
|
1060
|
+
if (!q) return;
|
|
1061
|
+
|
|
1062
|
+
const resultsEl = document.getElementById('search-results');
|
|
1063
|
+
const metaEl = document.getElementById('search-meta');
|
|
1064
|
+
resultsEl.innerHTML = '<div class="loading">Searching...</div>';
|
|
1065
|
+
metaEl.textContent = '';
|
|
1066
|
+
|
|
1067
|
+
try {
|
|
1068
|
+
const data = await apiFetch('/api/search?q=' + encodeURIComponent(q));
|
|
1069
|
+
const { results, count, elapsed_ms } = data;
|
|
1070
|
+
|
|
1071
|
+
metaEl.textContent = count + ' result' + (count !== 1 ? 's' : '') + ' in ' + elapsed_ms + 'ms';
|
|
1072
|
+
|
|
1073
|
+
if (!results || !results.length) {
|
|
1074
|
+
resultsEl.innerHTML = '<div class="empty-state">No results for "' + escHtml(q) + '"</div>';
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
resultsEl.innerHTML = results.map(r => {
|
|
1079
|
+
const layer = r.layer || (r.source?.startsWith('wiki') ? 'wiki' : 'memvid');
|
|
1080
|
+
const client = extractClient(r);
|
|
1081
|
+
const score = typeof r.score === 'number' ? r.score.toFixed(1) : r.score;
|
|
1082
|
+
const clientStr = client ? `<span class="badge badge-client">${escHtml(client)}</span>` : '';
|
|
1083
|
+
const tags = (r.tags || []).slice(0, 3).map(t => `<span class="badge badge-type">${escHtml(t)}</span>`).join('');
|
|
1084
|
+
|
|
1085
|
+
return `<div class="result-card" onclick="openSearchResult('${escAttr(r.wiki_path || '')}', '${escAttr(r.title || '')}')">
|
|
1086
|
+
<div class="result-header">
|
|
1087
|
+
<span class="badge badge-${layer}">${layer}</span>
|
|
1088
|
+
${clientStr}
|
|
1089
|
+
${tags}
|
|
1090
|
+
<span class="result-score">score: ${score}</span>
|
|
1091
|
+
</div>
|
|
1092
|
+
<div class="result-title">${escHtml(r.title || r.wiki_path || 'Untitled')}</div>
|
|
1093
|
+
<div class="result-snippet">${escHtml((r.text || '').slice(0, 300))}</div>
|
|
1094
|
+
${r.wiki_path ? '<div class="result-path">' + escHtml(r.wiki_path) + '</div>' : ''}
|
|
1095
|
+
</div>`;
|
|
1096
|
+
}).join('');
|
|
1097
|
+
|
|
1098
|
+
} catch(e) {
|
|
1099
|
+
resultsEl.innerHTML = '<div class="empty-state">Search error: ' + escHtml(e.message) + '</div>';
|
|
1100
|
+
metaEl.textContent = '';
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function extractClient(r) {
|
|
1105
|
+
if (r.wiki_path) {
|
|
1106
|
+
const parts = r.wiki_path.split('/');
|
|
1107
|
+
if (parts.length >= 2) return parts[parts.length - 2];
|
|
1108
|
+
}
|
|
1109
|
+
if (r.tags && r.tags.length) return r.tags[0];
|
|
1110
|
+
return '';
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function openSearchResult(wikiPath, title) {
|
|
1114
|
+
if (!wikiPath) return;
|
|
1115
|
+
// Strip 'wiki/' prefix if present, also handle 'wiki/client/page' format
|
|
1116
|
+
let clean = wikiPath.replace(/^wiki\//, '').replace(/\.md$/, '');
|
|
1117
|
+
switchTab('wiki');
|
|
1118
|
+
setTimeout(() => loadWikiPage(clean), 100);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// ---- WIKI ----
|
|
1122
|
+
async function loadWikiTree() {
|
|
1123
|
+
const treeEl = document.getElementById('wiki-tree');
|
|
1124
|
+
try {
|
|
1125
|
+
const data = await apiFetch('/api/wiki');
|
|
1126
|
+
wikiTree = data.tree || {};
|
|
1127
|
+
renderWikiTree();
|
|
1128
|
+
} catch(e) {
|
|
1129
|
+
treeEl.innerHTML = '<div style="padding:12px;font-size:12px;color:var(--red)">Error: ' + escHtml(e.message) + '</div>';
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function renderWikiTree() {
|
|
1134
|
+
const treeEl = document.getElementById('wiki-tree');
|
|
1135
|
+
const clients = Object.keys(wikiTree).filter(c => !c.startsWith('_') || c === '_shared');
|
|
1136
|
+
const special = Object.keys(wikiTree).filter(c => c.startsWith('_') && c !== '_shared');
|
|
1137
|
+
const allGroups = [...clients.sort(), ...special.sort()];
|
|
1138
|
+
|
|
1139
|
+
if (!allGroups.length) {
|
|
1140
|
+
treeEl.innerHTML = '<div class="empty-state" style="padding:12px">No wiki pages found</div>';
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
treeEl.innerHTML = allGroups.map(client => {
|
|
1145
|
+
const pages = wikiTree[client] || [];
|
|
1146
|
+
return `
|
|
1147
|
+
<div class="client-group">
|
|
1148
|
+
<div class="client-group-name" id="group-${client}" onclick="toggleWikiGroup('${client}')">
|
|
1149
|
+
<span class="toggle">▸</span>
|
|
1150
|
+
${slugToTitle(client)}
|
|
1151
|
+
<span style="margin-left:auto;font-size:10px;color:var(--text-dim);font-weight:400">${pages.length}</span>
|
|
1152
|
+
</div>
|
|
1153
|
+
<div class="client-pages" id="pages-${client}">
|
|
1154
|
+
${pages.map(p => `<div class="wiki-page-link" id="link-${escAttr(p.slug)}" onclick="loadWikiPage('${escAttr(p.slug)}')">${escHtml(p.title)}</div>`).join('')}
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>`;
|
|
1157
|
+
}).join('');
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function toggleWikiGroup(client) {
|
|
1161
|
+
const pages = document.getElementById('pages-' + client);
|
|
1162
|
+
const group = document.getElementById('group-' + client);
|
|
1163
|
+
if (pages) {
|
|
1164
|
+
const isOpen = pages.classList.toggle('open');
|
|
1165
|
+
group.classList.toggle('open', isOpen);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async function loadWikiPage(slug) {
|
|
1170
|
+
// Update active link
|
|
1171
|
+
document.querySelectorAll('.wiki-page-link').forEach(l => l.classList.remove('active'));
|
|
1172
|
+
const linkEl = document.getElementById('link-' + slug);
|
|
1173
|
+
if (linkEl) {
|
|
1174
|
+
linkEl.classList.add('active');
|
|
1175
|
+
// Expand parent group
|
|
1176
|
+
const group = linkEl.closest('.client-pages');
|
|
1177
|
+
if (group) {
|
|
1178
|
+
group.classList.add('open');
|
|
1179
|
+
const header = group.previousElementSibling;
|
|
1180
|
+
if (header) header.classList.add('open');
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const content = document.getElementById('wiki-content');
|
|
1185
|
+
content.innerHTML = '<div class="loading" style="padding:28px">Loading...</div>';
|
|
1186
|
+
currentPage = slug;
|
|
1187
|
+
|
|
1188
|
+
try {
|
|
1189
|
+
const data = await apiFetch('/api/wiki/' + encodeURIComponent(slug));
|
|
1190
|
+
renderWikiPage(data);
|
|
1191
|
+
} catch(e) {
|
|
1192
|
+
content.innerHTML = `<div style="padding:28px;color:var(--red)">Error loading page: ${escHtml(e.message)}</div>`;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function renderWikiPage(data) {
|
|
1197
|
+
const { frontmatter: fm, body } = data;
|
|
1198
|
+
const content = document.getElementById('wiki-content');
|
|
1199
|
+
|
|
1200
|
+
// Metadata badges
|
|
1201
|
+
const badges = [];
|
|
1202
|
+
if (fm.type) badges.push(`<span class="badge badge-type">${escHtml(fm.type)}</span>`);
|
|
1203
|
+
if (fm.confidence) badges.push(`<span class="badge badge-${fm.confidence}">${escHtml(fm.confidence)}</span>`);
|
|
1204
|
+
if (fm.client) badges.push(`<span class="badge badge-client">${escHtml(fm.client)}</span>`);
|
|
1205
|
+
if (fm.updated) badges.push(`<span style="font-size:11px;color:var(--text-dim)">Updated: ${escHtml(fm.updated)}</span>`);
|
|
1206
|
+
const tags = fm.tags || [];
|
|
1207
|
+
const tagList = Array.isArray(tags) ? tags : [tags];
|
|
1208
|
+
tagList.forEach(t => badges.push(`<span class="badge badge-type">${escHtml(t)}</span>`));
|
|
1209
|
+
|
|
1210
|
+
const metaHtml = badges.length ? `<div class="page-meta">${badges.join('')}</div>` : '';
|
|
1211
|
+
const bodyHtml = renderMarkdown(body || '');
|
|
1212
|
+
|
|
1213
|
+
content.innerHTML = metaHtml + '<div id="page-body">' + bodyHtml + '</div>';
|
|
1214
|
+
|
|
1215
|
+
// Wire up wikilinks
|
|
1216
|
+
document.querySelectorAll('#page-body a.wikilink').forEach(a => {
|
|
1217
|
+
const target = a.dataset.target;
|
|
1218
|
+
if (target) {
|
|
1219
|
+
a.onclick = (e) => { e.preventDefault(); loadWikiPage(target); };
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// ---- SIMPLE MARKDOWN RENDERER ----
|
|
1225
|
+
function renderMarkdown(md) {
|
|
1226
|
+
if (!md) return '';
|
|
1227
|
+
let html = md;
|
|
1228
|
+
|
|
1229
|
+
// Code blocks (fenced) - process FIRST before anything else munges them
|
|
1230
|
+
const codeBlocks = [];
|
|
1231
|
+
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
|
|
1232
|
+
const idx = codeBlocks.length;
|
|
1233
|
+
codeBlocks.push(`<pre><code class="lang-${escHtml(lang)}">${escHtml(code.trim())}</code></pre>`);
|
|
1234
|
+
return `\x00CODE${idx}\x00`;
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
// Inline code
|
|
1238
|
+
html = html.replace(/`([^`\n]+)`/g, (_, c) => `<code>${escHtml(c)}</code>`);
|
|
1239
|
+
|
|
1240
|
+
// Wikilinks [[path]] - before regular links
|
|
1241
|
+
html = html.replace(/\[\[([^\]]+)\]\]/g, (_, target) => {
|
|
1242
|
+
const parts = target.split('|');
|
|
1243
|
+
const path = parts[0].trim();
|
|
1244
|
+
const label = parts[1] ? parts[1].trim() : path.split('/').pop();
|
|
1245
|
+
return `<a class="wikilink" data-target="${escAttr(path)}" href="#">${escHtml(label)}</a>`;
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
// Bold
|
|
1249
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
1250
|
+
// Italic
|
|
1251
|
+
html = html.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
|
|
1252
|
+
// Strikethrough
|
|
1253
|
+
html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
|
1254
|
+
|
|
1255
|
+
// Tables
|
|
1256
|
+
html = html.replace(/((?:\|[^\n]+\|\n)+)/g, (tableBlock) => {
|
|
1257
|
+
const rows = tableBlock.trim().split('\n');
|
|
1258
|
+
if (rows.length < 2) return tableBlock;
|
|
1259
|
+
const headerCells = rows[0].split('|').filter((_, i, a) => i > 0 && i < a.length - 1);
|
|
1260
|
+
const isSeparator = (r) => /^\s*[\|\-\s:]+\s*$/.test(r);
|
|
1261
|
+
let dataRows = rows.slice(1).filter(r => !isSeparator(r));
|
|
1262
|
+
const thead = '<thead><tr>' + headerCells.map(c => `<th>${c.trim()}</th>`).join('') + '</tr></thead>';
|
|
1263
|
+
const tbody = '<tbody>' + dataRows.map(r => {
|
|
1264
|
+
const cells = r.split('|').filter((_, i, a) => i > 0 && i < a.length - 1);
|
|
1265
|
+
return '<tr>' + cells.map(c => `<td>${c.trim()}</td>`).join('') + '</tr>';
|
|
1266
|
+
}).join('') + '</tbody>';
|
|
1267
|
+
return '<table>' + thead + tbody + '</table>';
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
// Headings
|
|
1271
|
+
html = html.replace(/^######\s+(.+)$/gm, '<h6>$1</h6>');
|
|
1272
|
+
html = html.replace(/^#####\s+(.+)$/gm, '<h5>$1</h5>');
|
|
1273
|
+
html = html.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>');
|
|
1274
|
+
html = html.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
|
|
1275
|
+
html = html.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>');
|
|
1276
|
+
html = html.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>');
|
|
1277
|
+
|
|
1278
|
+
// Blockquotes
|
|
1279
|
+
html = html.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
|
|
1280
|
+
|
|
1281
|
+
// HR
|
|
1282
|
+
html = html.replace(/^---+$/gm, '<hr>');
|
|
1283
|
+
|
|
1284
|
+
// Unordered lists (group consecutive items)
|
|
1285
|
+
html = html.replace(/((?:^[ \t]*[-*+]\s+.+\n?)+)/gm, (block) => {
|
|
1286
|
+
const items = block.trim().split('\n').map(l => l.replace(/^[ \t]*[-*+]\s+/, '').trim());
|
|
1287
|
+
return '<ul>' + items.map(i => `<li>${i}</li>`).join('') + '</ul>\n';
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
// Ordered lists
|
|
1291
|
+
html = html.replace(/((?:^\d+\.\s+.+\n?)+)/gm, (block) => {
|
|
1292
|
+
const items = block.trim().split('\n').map(l => l.replace(/^\d+\.\s+/, '').trim());
|
|
1293
|
+
return '<ol>' + items.map(i => `<li>${i}</li>`).join('') + '</ol>\n';
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
// Paragraphs - wrap lines separated by blank lines
|
|
1297
|
+
html = html.replace(/\n\n+/g, '\n\n');
|
|
1298
|
+
const paragraphs = html.split('\n\n');
|
|
1299
|
+
html = paragraphs.map(block => {
|
|
1300
|
+
const trimmed = block.trim();
|
|
1301
|
+
if (!trimmed) return '';
|
|
1302
|
+
if (/^<(h[1-6]|ul|ol|pre|table|blockquote|hr|div)/.test(trimmed)) return trimmed;
|
|
1303
|
+
if (/\x00CODE\d+\x00/.test(trimmed)) return trimmed;
|
|
1304
|
+
return '<p>' + trimmed.replace(/\n/g, ' ') + '</p>';
|
|
1305
|
+
}).join('\n');
|
|
1306
|
+
|
|
1307
|
+
// Restore code blocks
|
|
1308
|
+
codeBlocks.forEach((block, idx) => {
|
|
1309
|
+
html = html.replace(`\x00CODE${idx}\x00`, block);
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
return html;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// ---- ENTITIES ----
|
|
1316
|
+
async function loadEntities() {
|
|
1317
|
+
try {
|
|
1318
|
+
const data = await apiFetch('/api/entities');
|
|
1319
|
+
allEntities = (data.entities || []).filter(e => e && e.name);
|
|
1320
|
+
|
|
1321
|
+
// Populate client filter
|
|
1322
|
+
const clients = [...new Set(allEntities.map(e => e.client).filter(Boolean))].sort();
|
|
1323
|
+
const sel = document.getElementById('entity-client-filter');
|
|
1324
|
+
clients.forEach(c => {
|
|
1325
|
+
const opt = document.createElement('option');
|
|
1326
|
+
opt.value = c;
|
|
1327
|
+
opt.textContent = slugToTitle(c);
|
|
1328
|
+
sel.appendChild(opt);
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
renderEntitiesTable(allEntities);
|
|
1332
|
+
} catch(e) {
|
|
1333
|
+
document.getElementById('entities-body').innerHTML =
|
|
1334
|
+
`<tr><td colspan="4" style="text-align:center;padding:20px;color:var(--red)">Error: ${escHtml(e.message)}</td></tr>`;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function filterEntities() {
|
|
1339
|
+
const q = document.getElementById('entity-filter').value.toLowerCase();
|
|
1340
|
+
const client = document.getElementById('entity-client-filter').value;
|
|
1341
|
+
|
|
1342
|
+
const filtered = allEntities.filter(e => {
|
|
1343
|
+
const matchName = !q || (e.name || '').toLowerCase().includes(q) || (e.id || '').toLowerCase().includes(q);
|
|
1344
|
+
const matchClient = !client || e.client === client;
|
|
1345
|
+
return matchName && matchClient;
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
renderEntitiesTable(filtered);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function renderEntitiesTable(entities) {
|
|
1352
|
+
const tbody = document.getElementById('entities-body');
|
|
1353
|
+
document.getElementById('entities-count').textContent =
|
|
1354
|
+
`Showing ${entities.length} of ${allEntities.length} entities`;
|
|
1355
|
+
|
|
1356
|
+
if (!entities.length) {
|
|
1357
|
+
tbody.innerHTML = '<tr><td colspan="4" class="empty-state" style="text-align:center;padding:20px">No entities match your filter</td></tr>';
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
tbody.innerHTML = entities.map(e => {
|
|
1362
|
+
const wikiLink = e.wiki_page
|
|
1363
|
+
? `<span class="entity-link" onclick="openEntityPage('${escAttr(e.wiki_page)}')">${escHtml(e.wiki_page)}</span>`
|
|
1364
|
+
: '<span style="color:var(--text-dim)">-</span>';
|
|
1365
|
+
return `<tr>
|
|
1366
|
+
<td><span class="entity-name">${escHtml(e.name || e.id)}</span></td>
|
|
1367
|
+
<td><span class="badge badge-client">${escHtml(e.client || '-')}</span></td>
|
|
1368
|
+
<td><span class="entity-type">${escHtml(e.type || 'unknown')}</span></td>
|
|
1369
|
+
<td>${wikiLink}</td>
|
|
1370
|
+
</tr>`;
|
|
1371
|
+
}).join('');
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function openEntityPage(wikiPage) {
|
|
1375
|
+
const slug = wikiPage.replace(/\.md$/, '');
|
|
1376
|
+
switchTab('wiki');
|
|
1377
|
+
setTimeout(() => loadWikiPage(slug), 100);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// ---- HEALTH ----
|
|
1381
|
+
async function runHealthCheck() {
|
|
1382
|
+
const btn = document.getElementById('health-btn');
|
|
1383
|
+
const status = document.getElementById('health-run-status');
|
|
1384
|
+
const results = document.getElementById('health-results');
|
|
1385
|
+
|
|
1386
|
+
btn.disabled = true;
|
|
1387
|
+
btn.textContent = 'Running...';
|
|
1388
|
+
status.textContent = '';
|
|
1389
|
+
results.innerHTML = '<div class="loading">Running drift check...</div>';
|
|
1390
|
+
|
|
1391
|
+
try {
|
|
1392
|
+
const data = await apiFetch('/api/drift');
|
|
1393
|
+
renderHealthResults(data);
|
|
1394
|
+
status.textContent = 'Completed at ' + new Date().toLocaleTimeString();
|
|
1395
|
+
} catch(e) {
|
|
1396
|
+
results.innerHTML = `<div class="empty-state" style="color:var(--red)">Health check failed: ${escHtml(e.message)}</div>`;
|
|
1397
|
+
status.textContent = 'Failed';
|
|
1398
|
+
} finally {
|
|
1399
|
+
btn.disabled = false;
|
|
1400
|
+
btn.textContent = 'Run Health Check';
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function renderHealthResults(data) {
|
|
1405
|
+
const results = document.getElementById('health-results');
|
|
1406
|
+
const summary = data.summary || {};
|
|
1407
|
+
const syncPct = typeof summary.sync_pct === 'number' ? summary.sync_pct : null;
|
|
1408
|
+
const missing = data.missing_from_memvid || [];
|
|
1409
|
+
const orphans = data.orphan_frames || [];
|
|
1410
|
+
const stale = data.stale || [];
|
|
1411
|
+
const error = data.error;
|
|
1412
|
+
|
|
1413
|
+
if (error && !summary.sync_pct) {
|
|
1414
|
+
results.innerHTML = `<div class="card" style="color:var(--amber);font-size:13px"><strong>Note:</strong> ${escHtml(error)}</div>`;
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
const pctColor = syncPct === null ? 'var(--text-dim)' : syncPct >= 90 ? 'var(--green)' : syncPct >= 70 ? 'var(--amber)' : 'var(--red)';
|
|
1419
|
+
const barClass = syncPct === null ? '' : syncPct >= 90 ? 'green' : syncPct >= 70 ? 'amber' : 'red';
|
|
1420
|
+
const barWidth = syncPct !== null ? syncPct : 0;
|
|
1421
|
+
|
|
1422
|
+
const scoreHtml = syncPct !== null ? `
|
|
1423
|
+
<div class="health-status-bar">
|
|
1424
|
+
<div class="health-score">
|
|
1425
|
+
<div class="health-score-value" style="color:${pctColor}">${syncPct}%</div>
|
|
1426
|
+
<div>
|
|
1427
|
+
<div class="health-score-label">Sync coverage</div>
|
|
1428
|
+
<div style="font-size:12px;color:var(--text-dim)">${summary.synced ?? '-'} of ${summary.total_wiki_pages ?? '-'} pages in memvid</div>
|
|
1429
|
+
</div>
|
|
1430
|
+
</div>
|
|
1431
|
+
<div class="progress-bar">
|
|
1432
|
+
<div class="progress-fill ${barClass}" style="width:${barWidth}%"></div>
|
|
1433
|
+
</div>
|
|
1434
|
+
<div style="font-size:11px;color:var(--text-dim);margin-top:4px">${summary.memvid_frame_count ?? '-'} total frames in master archive</div>
|
|
1435
|
+
</div>` : '';
|
|
1436
|
+
|
|
1437
|
+
const makeSectionHtml = (title, items, emptyMsg, icon, iconColor) => `
|
|
1438
|
+
<div class="health-section">
|
|
1439
|
+
<div class="health-section-title">
|
|
1440
|
+
<span style="color:${iconColor}">${icon}</span>
|
|
1441
|
+
${title}
|
|
1442
|
+
<span class="health-count">${items.length}</span>
|
|
1443
|
+
</div>
|
|
1444
|
+
<div class="health-list">
|
|
1445
|
+
${items.length === 0
|
|
1446
|
+
? `<div class="health-empty">${emptyMsg}</div>`
|
|
1447
|
+
: items.slice(0, 20).map(item => {
|
|
1448
|
+
const text = typeof item === 'string' ? item : (item.wiki_path || JSON.stringify(item));
|
|
1449
|
+
const detail = typeof item === 'object' && item.issue ? ` - ${item.issue}` : '';
|
|
1450
|
+
return `<div class="health-list-item"><span class="icon">${icon}</span>${escHtml(text)}${detail ? `<span style="color:var(--text-dim)">${escHtml(detail)}</span>` : ''}</div>`;
|
|
1451
|
+
}).join('') + (items.length > 20 ? `<div class="health-empty">+${items.length - 20} more</div>` : '')
|
|
1452
|
+
}
|
|
1453
|
+
</div>
|
|
1454
|
+
</div>`;
|
|
1455
|
+
|
|
1456
|
+
results.innerHTML = scoreHtml +
|
|
1457
|
+
makeSectionHtml('Missing from Memvid', missing, 'All pages are synced to memvid', '△', 'var(--red)') +
|
|
1458
|
+
makeSectionHtml('Orphan Frames', orphans, 'No orphan frames found', '■', 'var(--amber)') +
|
|
1459
|
+
makeSectionHtml('Recently Modified (may be stale)', stale, 'No recently modified pages detected', '△', 'var(--amber)');
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
async function runSync() {
|
|
1463
|
+
const btn = document.getElementById('sync-btn');
|
|
1464
|
+
const status = document.getElementById('health-run-status');
|
|
1465
|
+
|
|
1466
|
+
btn.disabled = true;
|
|
1467
|
+
btn.textContent = 'Syncing...';
|
|
1468
|
+
status.textContent = '';
|
|
1469
|
+
|
|
1470
|
+
try {
|
|
1471
|
+
const data = await apiPost('/api/sync');
|
|
1472
|
+
const msg = `Sync complete: ${data.total_pages} pages, ${data.total_frames} frames`;
|
|
1473
|
+
status.textContent = msg;
|
|
1474
|
+
if (data.errors && data.errors.length) {
|
|
1475
|
+
status.textContent += ` (${data.errors.length} errors)`;
|
|
1476
|
+
}
|
|
1477
|
+
// Refresh stats
|
|
1478
|
+
loadDashboard();
|
|
1479
|
+
} catch(e) {
|
|
1480
|
+
status.textContent = 'Sync failed: ' + e.message;
|
|
1481
|
+
} finally {
|
|
1482
|
+
btn.disabled = false;
|
|
1483
|
+
btn.textContent = 'Sync All Pages';
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// ---- ESCAPE HELPERS ----
|
|
1488
|
+
function escHtml(s) {
|
|
1489
|
+
if (s === null || s === undefined) return '';
|
|
1490
|
+
return String(s)
|
|
1491
|
+
.replace(/&/g, '&')
|
|
1492
|
+
.replace(/</g, '<')
|
|
1493
|
+
.replace(/>/g, '>')
|
|
1494
|
+
.replace(/"/g, '"');
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function escAttr(s) {
|
|
1498
|
+
if (s === null || s === undefined) return '';
|
|
1499
|
+
return String(s).replace(/'/g, ''').replace(/"/g, '"');
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// ---- INIT ----
|
|
1503
|
+
(async function init() {
|
|
1504
|
+
// Load all data
|
|
1505
|
+
loadDashboard();
|
|
1506
|
+
loadWikiTree();
|
|
1507
|
+
loadEntities();
|
|
1508
|
+
|
|
1509
|
+
// Keyboard shortcut: / focuses search
|
|
1510
|
+
document.addEventListener('keydown', e => {
|
|
1511
|
+
if (e.key === '/' && document.activeElement.tagName !== 'INPUT') {
|
|
1512
|
+
e.preventDefault();
|
|
1513
|
+
switchTab('search');
|
|
1514
|
+
document.getElementById('search-input').focus();
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
})();
|
|
1518
|
+
</script>
|
|
1519
|
+
</body>
|
|
1520
|
+
</html>
|