context-vault 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +383 -0
- package/bin/cli.js +588 -0
- package/package.json +30 -0
- package/smithery.yaml +10 -0
- package/src/capture/README.md +23 -0
- package/src/capture/file-ops.js +75 -0
- package/src/capture/formatters.js +29 -0
- package/src/capture/index.js +91 -0
- package/src/core/README.md +20 -0
- package/src/core/categories.js +50 -0
- package/src/core/config.js +76 -0
- package/src/core/files.js +114 -0
- package/src/core/frontmatter.js +108 -0
- package/src/core/status.js +105 -0
- package/src/index/README.md +28 -0
- package/src/index/db.js +138 -0
- package/src/index/embed.js +56 -0
- package/src/index/index.js +258 -0
- package/src/retrieve/README.md +19 -0
- package/src/retrieve/index.js +173 -0
- package/src/server/README.md +44 -0
- package/src/server/helpers.js +29 -0
- package/src/server/index.js +82 -0
- package/src/server/tools.js +211 -0
- package/ui/Context.applescript +36 -0
- package/ui/index.html +1377 -0
- package/ui/serve.js +473 -0
package/ui/index.html
ADDED
|
@@ -0,0 +1,1377 @@
|
|
|
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">
|
|
6
|
+
<title>context-mcp</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
font-family: -apple-system, system-ui, 'Segoe UI', sans-serif;
|
|
12
|
+
background: #0a0a0a;
|
|
13
|
+
color: #e5e5e5;
|
|
14
|
+
line-height: 1.5;
|
|
15
|
+
min-height: 100vh;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* ─── Layout ──────────────────────────────────────────────── */
|
|
19
|
+
|
|
20
|
+
.app {
|
|
21
|
+
display: grid;
|
|
22
|
+
grid-template-columns: 240px 1fr;
|
|
23
|
+
grid-template-rows: auto 1fr;
|
|
24
|
+
min-height: 100vh;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
header {
|
|
28
|
+
grid-column: 1 / -1;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: space-between;
|
|
32
|
+
padding: 12px 20px;
|
|
33
|
+
border-bottom: 1px solid #222;
|
|
34
|
+
background: #111;
|
|
35
|
+
gap: 12px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
header h1 {
|
|
39
|
+
font-size: 15px;
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
color: #999;
|
|
42
|
+
letter-spacing: 0.5px;
|
|
43
|
+
white-space: nowrap;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
header h1 span { color: #e5e5e5; }
|
|
47
|
+
|
|
48
|
+
.header-right {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: 10px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.connection-pill {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 6px;
|
|
58
|
+
font-size: 11px;
|
|
59
|
+
color: #666;
|
|
60
|
+
background: #1a1a1a;
|
|
61
|
+
border: 1px solid #2a2a2a;
|
|
62
|
+
padding: 4px 10px;
|
|
63
|
+
border-radius: 12px;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
white-space: nowrap;
|
|
66
|
+
max-width: 300px;
|
|
67
|
+
overflow: hidden;
|
|
68
|
+
text-overflow: ellipsis;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.connection-pill:hover { border-color: #444; color: #999; }
|
|
72
|
+
|
|
73
|
+
.connection-dot {
|
|
74
|
+
width: 6px;
|
|
75
|
+
height: 6px;
|
|
76
|
+
border-radius: 50%;
|
|
77
|
+
background: #3b7;
|
|
78
|
+
flex-shrink: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.connection-dot.disconnected { background: #c44; }
|
|
82
|
+
|
|
83
|
+
.search-box {
|
|
84
|
+
background: #1a1a1a;
|
|
85
|
+
border: 1px solid #333;
|
|
86
|
+
border-radius: 6px;
|
|
87
|
+
color: #e5e5e5;
|
|
88
|
+
padding: 6px 12px;
|
|
89
|
+
font-size: 13px;
|
|
90
|
+
width: 300px;
|
|
91
|
+
outline: none;
|
|
92
|
+
font-family: inherit;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.search-box:focus { border-color: #555; }
|
|
96
|
+
.search-box::placeholder { color: #555; }
|
|
97
|
+
|
|
98
|
+
/* ─── Sidebar ─────────────────────────────────────────────── */
|
|
99
|
+
|
|
100
|
+
.sidebar {
|
|
101
|
+
border-right: 1px solid #222;
|
|
102
|
+
padding: 12px 0;
|
|
103
|
+
overflow-y: auto;
|
|
104
|
+
background: #0e0e0e;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.sidebar-section {
|
|
108
|
+
padding: 6px 16px;
|
|
109
|
+
font-size: 10px;
|
|
110
|
+
text-transform: uppercase;
|
|
111
|
+
letter-spacing: 1px;
|
|
112
|
+
color: #555;
|
|
113
|
+
margin-top: 8px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.sidebar-section:first-child { margin-top: 0; }
|
|
117
|
+
|
|
118
|
+
.sidebar a {
|
|
119
|
+
display: flex;
|
|
120
|
+
justify-content: space-between;
|
|
121
|
+
align-items: center;
|
|
122
|
+
padding: 6px 16px;
|
|
123
|
+
color: #aaa;
|
|
124
|
+
text-decoration: none;
|
|
125
|
+
font-size: 13px;
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
border-left: 3px solid transparent;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.sidebar a:hover { background: #1a1a1a; color: #e5e5e5; }
|
|
131
|
+
.sidebar a.active { color: #fff; border-left-color: #fff; background: #1a1a1a; }
|
|
132
|
+
|
|
133
|
+
.sidebar a.indent { padding-left: 28px; font-size: 12px; color: #777; }
|
|
134
|
+
.sidebar a.indent:hover { color: #ccc; }
|
|
135
|
+
.sidebar a.indent.active { color: #fff; }
|
|
136
|
+
|
|
137
|
+
.sidebar .count {
|
|
138
|
+
font-size: 11px;
|
|
139
|
+
color: #555;
|
|
140
|
+
background: #1a1a1a;
|
|
141
|
+
padding: 1px 6px;
|
|
142
|
+
border-radius: 8px;
|
|
143
|
+
flex-shrink: 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.sidebar a.active .count { color: #888; }
|
|
147
|
+
|
|
148
|
+
.sidebar .db-info {
|
|
149
|
+
padding: 12px 16px;
|
|
150
|
+
font-size: 11px;
|
|
151
|
+
color: #444;
|
|
152
|
+
border-top: 1px solid #222;
|
|
153
|
+
margin-top: 12px;
|
|
154
|
+
word-break: break-all;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.sidebar .db-info div { margin-bottom: 2px; }
|
|
158
|
+
|
|
159
|
+
/* ─── Main Content ────────────────────────────────────────── */
|
|
160
|
+
|
|
161
|
+
.main {
|
|
162
|
+
padding: 16px 24px;
|
|
163
|
+
overflow-y: auto;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.main-header {
|
|
167
|
+
display: flex;
|
|
168
|
+
justify-content: space-between;
|
|
169
|
+
align-items: center;
|
|
170
|
+
margin-bottom: 16px;
|
|
171
|
+
font-size: 12px;
|
|
172
|
+
color: #555;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ─── Row Cards (generic) ────────────────────────────────── */
|
|
176
|
+
|
|
177
|
+
.row-card {
|
|
178
|
+
background: #141414;
|
|
179
|
+
border: 1px solid #222;
|
|
180
|
+
border-radius: 8px;
|
|
181
|
+
padding: 14px 16px;
|
|
182
|
+
margin-bottom: 8px;
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
transition: border-color 0.15s;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.row-card:hover { border-color: #333; }
|
|
188
|
+
.row-card.expanded { border-color: #444; }
|
|
189
|
+
|
|
190
|
+
.row-title {
|
|
191
|
+
font-size: 14px;
|
|
192
|
+
font-weight: 500;
|
|
193
|
+
color: #e5e5e5;
|
|
194
|
+
margin-bottom: 6px;
|
|
195
|
+
line-height: 1.4;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.row-meta {
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 8px;
|
|
202
|
+
flex-wrap: wrap;
|
|
203
|
+
font-size: 12px;
|
|
204
|
+
color: #666;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.badge {
|
|
208
|
+
font-size: 11px;
|
|
209
|
+
padding: 1px 8px;
|
|
210
|
+
border-radius: 4px;
|
|
211
|
+
font-weight: 500;
|
|
212
|
+
color: #fff;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.tag {
|
|
216
|
+
font-size: 11px;
|
|
217
|
+
color: #666;
|
|
218
|
+
background: #1a1a1a;
|
|
219
|
+
padding: 1px 6px;
|
|
220
|
+
border-radius: 3px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.row-detail {
|
|
224
|
+
display: none;
|
|
225
|
+
margin-top: 12px;
|
|
226
|
+
padding-top: 12px;
|
|
227
|
+
border-top: 1px solid #222;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.row-card.expanded .row-detail { display: block; }
|
|
231
|
+
|
|
232
|
+
.row-body {
|
|
233
|
+
font-size: 13px;
|
|
234
|
+
color: #ccc;
|
|
235
|
+
word-break: break-word;
|
|
236
|
+
line-height: 1.6;
|
|
237
|
+
max-height: 400px;
|
|
238
|
+
overflow-y: auto;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.row-body.raw { white-space: pre-wrap; }
|
|
242
|
+
|
|
243
|
+
.row-body h1, .row-body h2, .row-body h3 { color: #e5e5e5; margin: 12px 0 6px; }
|
|
244
|
+
.row-body h1 { font-size: 16px; }
|
|
245
|
+
.row-body h2 { font-size: 14px; }
|
|
246
|
+
.row-body h3 { font-size: 13px; }
|
|
247
|
+
.row-body p { margin: 6px 0; }
|
|
248
|
+
.row-body ul, .row-body ol { padding-left: 20px; margin: 6px 0; }
|
|
249
|
+
.row-body code { background: #1a1a1a; padding: 1px 4px; border-radius: 3px; font-size: 12px; }
|
|
250
|
+
.row-body pre { background: #1a1a1a; border: 1px solid #222; border-radius: 6px; padding: 10px; overflow-x: auto; margin: 8px 0; }
|
|
251
|
+
.row-body pre code { background: none; padding: 0; }
|
|
252
|
+
.row-body a { color: #6ad; }
|
|
253
|
+
.row-body blockquote { border-left: 3px solid #333; padding-left: 12px; color: #888; margin: 8px 0; }
|
|
254
|
+
.row-body table { border-collapse: collapse; margin: 8px 0; }
|
|
255
|
+
.row-body th, .row-body td { border: 1px solid #333; padding: 4px 8px; font-size: 12px; }
|
|
256
|
+
.row-body th { background: #1a1a1a; color: #aaa; }
|
|
257
|
+
|
|
258
|
+
.raw-toggle {
|
|
259
|
+
font-size: 11px;
|
|
260
|
+
color: #666;
|
|
261
|
+
background: #1a1a1a;
|
|
262
|
+
border: 1px solid #2a2a2a;
|
|
263
|
+
padding: 2px 8px;
|
|
264
|
+
border-radius: 3px;
|
|
265
|
+
cursor: pointer;
|
|
266
|
+
float: right;
|
|
267
|
+
margin-bottom: 6px;
|
|
268
|
+
}
|
|
269
|
+
.raw-toggle:hover { border-color: #444; color: #999; }
|
|
270
|
+
|
|
271
|
+
.file-preview.rendered h1, .file-preview.rendered h2, .file-preview.rendered h3 { color: #e5e5e5; margin: 12px 0 6px; }
|
|
272
|
+
.file-preview.rendered p { margin: 6px 0; }
|
|
273
|
+
.file-preview.rendered code { background: #1a1a1a; padding: 1px 4px; border-radius: 3px; font-size: 12px; }
|
|
274
|
+
.file-preview.rendered pre { background: #0a0a0a; border: 1px solid #222; border-radius: 6px; padding: 10px; overflow-x: auto; margin: 8px 0; }
|
|
275
|
+
.file-preview.rendered pre code { background: none; padding: 0; }
|
|
276
|
+
.file-preview.rendered a { color: #6ad; }
|
|
277
|
+
.file-preview.rendered ul, .file-preview.rendered ol { padding-left: 20px; margin: 6px 0; }
|
|
278
|
+
.file-preview.rendered blockquote { border-left: 3px solid #333; padding-left: 12px; color: #888; margin: 8px 0; }
|
|
279
|
+
.file-preview.rendered { white-space: normal; font-family: -apple-system, system-ui, 'Segoe UI', sans-serif; }
|
|
280
|
+
|
|
281
|
+
.row-field {
|
|
282
|
+
margin-top: 8px;
|
|
283
|
+
font-size: 12px;
|
|
284
|
+
color: #555;
|
|
285
|
+
line-height: 1.5;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.row-field strong { color: #777; }
|
|
289
|
+
|
|
290
|
+
/* ─── Settings View ──────────────────────────────────────── */
|
|
291
|
+
|
|
292
|
+
.settings-section {
|
|
293
|
+
margin-bottom: 24px;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.settings-section h2 {
|
|
297
|
+
font-size: 14px;
|
|
298
|
+
font-weight: 600;
|
|
299
|
+
color: #ccc;
|
|
300
|
+
margin-bottom: 12px;
|
|
301
|
+
padding-bottom: 8px;
|
|
302
|
+
border-bottom: 1px solid #222;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.settings-field {
|
|
306
|
+
margin-bottom: 12px;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.settings-field label {
|
|
310
|
+
display: block;
|
|
311
|
+
font-size: 12px;
|
|
312
|
+
color: #888;
|
|
313
|
+
margin-bottom: 4px;
|
|
314
|
+
font-weight: 500;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.settings-field input {
|
|
318
|
+
width: 100%;
|
|
319
|
+
background: #1a1a1a;
|
|
320
|
+
border: 1px solid #333;
|
|
321
|
+
border-radius: 6px;
|
|
322
|
+
color: #e5e5e5;
|
|
323
|
+
padding: 8px 12px;
|
|
324
|
+
font-size: 13px;
|
|
325
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
326
|
+
outline: none;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.settings-field input:focus { border-color: #555; }
|
|
330
|
+
|
|
331
|
+
.settings-field .field-hint {
|
|
332
|
+
font-size: 11px;
|
|
333
|
+
color: #444;
|
|
334
|
+
margin-top: 3px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.settings-field .field-status {
|
|
338
|
+
font-size: 11px;
|
|
339
|
+
margin-top: 3px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.field-status.ok { color: #3b7; }
|
|
343
|
+
.field-status.warn { color: #c93; }
|
|
344
|
+
|
|
345
|
+
.settings-actions {
|
|
346
|
+
display: flex;
|
|
347
|
+
gap: 8px;
|
|
348
|
+
margin-top: 16px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.btn {
|
|
352
|
+
background: #1a1a1a;
|
|
353
|
+
border: 1px solid #333;
|
|
354
|
+
color: #ccc;
|
|
355
|
+
padding: 6px 16px;
|
|
356
|
+
border-radius: 6px;
|
|
357
|
+
cursor: pointer;
|
|
358
|
+
font-size: 13px;
|
|
359
|
+
font-family: inherit;
|
|
360
|
+
transition: all 0.15s;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.btn:hover { border-color: #555; color: #fff; }
|
|
364
|
+
|
|
365
|
+
.btn-primary {
|
|
366
|
+
background: #1a3a2a;
|
|
367
|
+
border-color: #2a5a3a;
|
|
368
|
+
color: #6d9;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.btn-primary:hover { background: #1d4430; border-color: #3a7a4a; }
|
|
372
|
+
|
|
373
|
+
.settings-toast {
|
|
374
|
+
padding: 8px 12px;
|
|
375
|
+
border-radius: 6px;
|
|
376
|
+
font-size: 12px;
|
|
377
|
+
margin-top: 12px;
|
|
378
|
+
display: none;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.settings-toast.success { display: block; background: #1a3a2a; border: 1px solid #2a5a3a; color: #6d9; }
|
|
382
|
+
.settings-toast.error { display: block; background: #3a1a1a; border: 1px solid #5a2a2a; color: #d66; }
|
|
383
|
+
|
|
384
|
+
/* ─── Schema Cards ────────────────────────────────────────── */
|
|
385
|
+
|
|
386
|
+
.table-card {
|
|
387
|
+
background: #141414;
|
|
388
|
+
border: 1px solid #222;
|
|
389
|
+
border-radius: 8px;
|
|
390
|
+
padding: 14px 16px;
|
|
391
|
+
margin-bottom: 8px;
|
|
392
|
+
cursor: pointer;
|
|
393
|
+
transition: border-color 0.15s;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.table-card:hover { border-color: #333; }
|
|
397
|
+
|
|
398
|
+
.schema-section-label {
|
|
399
|
+
font-size: 10px;
|
|
400
|
+
text-transform: uppercase;
|
|
401
|
+
letter-spacing: 1px;
|
|
402
|
+
color: #555;
|
|
403
|
+
margin: 16px 0 8px;
|
|
404
|
+
}
|
|
405
|
+
.schema-section-label:first-child { margin-top: 0; }
|
|
406
|
+
|
|
407
|
+
.internal-toggle {
|
|
408
|
+
cursor: pointer;
|
|
409
|
+
user-select: none;
|
|
410
|
+
display: inline-flex;
|
|
411
|
+
align-items: center;
|
|
412
|
+
gap: 4px;
|
|
413
|
+
}
|
|
414
|
+
.internal-toggle .arrow { transition: transform 0.15s; font-size: 9px; }
|
|
415
|
+
.internal-toggle .arrow.open { transform: rotate(90deg); }
|
|
416
|
+
|
|
417
|
+
.internal-tables { display: none; }
|
|
418
|
+
.internal-tables.open { display: block; }
|
|
419
|
+
|
|
420
|
+
.internal-tables .table-card {
|
|
421
|
+
opacity: 0.5;
|
|
422
|
+
padding: 8px 12px;
|
|
423
|
+
cursor: pointer;
|
|
424
|
+
}
|
|
425
|
+
.internal-tables .table-card:hover { opacity: 0.75; border-color: #333; }
|
|
426
|
+
.internal-tables .table-name { font-size: 12px; }
|
|
427
|
+
.internal-tables .table-columns { display: none; }
|
|
428
|
+
|
|
429
|
+
.table-name {
|
|
430
|
+
font-size: 14px;
|
|
431
|
+
font-weight: 500;
|
|
432
|
+
color: #e5e5e5;
|
|
433
|
+
margin-bottom: 6px;
|
|
434
|
+
display: flex;
|
|
435
|
+
align-items: center;
|
|
436
|
+
gap: 8px;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.table-name .badge {
|
|
440
|
+
font-size: 10px;
|
|
441
|
+
padding: 1px 6px;
|
|
442
|
+
border-radius: 3px;
|
|
443
|
+
background: #1a2a3a;
|
|
444
|
+
color: #6ad;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.table-columns {
|
|
448
|
+
display: flex;
|
|
449
|
+
gap: 6px;
|
|
450
|
+
flex-wrap: wrap;
|
|
451
|
+
margin-top: 6px;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.table-col {
|
|
455
|
+
font-size: 11px;
|
|
456
|
+
color: #888;
|
|
457
|
+
background: #1a1a1a;
|
|
458
|
+
padding: 2px 8px;
|
|
459
|
+
border-radius: 3px;
|
|
460
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.table-col .col-type {
|
|
464
|
+
color: #555;
|
|
465
|
+
margin-left: 4px;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* ─── File Browser ───────────────────────────────────────── */
|
|
469
|
+
|
|
470
|
+
.breadcrumb {
|
|
471
|
+
display: flex;
|
|
472
|
+
align-items: center;
|
|
473
|
+
gap: 4px;
|
|
474
|
+
margin-bottom: 12px;
|
|
475
|
+
font-size: 12px;
|
|
476
|
+
flex-wrap: wrap;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.breadcrumb a {
|
|
480
|
+
color: #6ad;
|
|
481
|
+
text-decoration: none;
|
|
482
|
+
cursor: pointer;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.breadcrumb a:hover { text-decoration: underline; }
|
|
486
|
+
.breadcrumb .sep { color: #444; }
|
|
487
|
+
|
|
488
|
+
.file-item {
|
|
489
|
+
display: flex;
|
|
490
|
+
align-items: center;
|
|
491
|
+
gap: 10px;
|
|
492
|
+
padding: 8px 12px;
|
|
493
|
+
border-radius: 6px;
|
|
494
|
+
cursor: pointer;
|
|
495
|
+
font-size: 13px;
|
|
496
|
+
color: #ccc;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.file-item:hover { background: #1a1a1a; }
|
|
500
|
+
|
|
501
|
+
.file-icon { font-size: 14px; width: 20px; text-align: center; flex-shrink: 0; }
|
|
502
|
+
.file-name { flex: 1; }
|
|
503
|
+
.file-meta { font-size: 11px; color: #555; }
|
|
504
|
+
|
|
505
|
+
.file-preview {
|
|
506
|
+
background: #141414;
|
|
507
|
+
border: 1px solid #222;
|
|
508
|
+
border-radius: 8px;
|
|
509
|
+
padding: 16px;
|
|
510
|
+
margin-top: 8px;
|
|
511
|
+
font-size: 13px;
|
|
512
|
+
color: #ccc;
|
|
513
|
+
white-space: pre-wrap;
|
|
514
|
+
word-break: break-word;
|
|
515
|
+
line-height: 1.6;
|
|
516
|
+
max-height: 600px;
|
|
517
|
+
overflow-y: auto;
|
|
518
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/* ─── Pagination ──────────────────────────────────────────── */
|
|
522
|
+
|
|
523
|
+
.pagination {
|
|
524
|
+
display: flex;
|
|
525
|
+
gap: 8px;
|
|
526
|
+
margin-top: 16px;
|
|
527
|
+
justify-content: center;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.pagination button {
|
|
531
|
+
background: #1a1a1a;
|
|
532
|
+
border: 1px solid #333;
|
|
533
|
+
color: #aaa;
|
|
534
|
+
padding: 4px 14px;
|
|
535
|
+
border-radius: 4px;
|
|
536
|
+
cursor: pointer;
|
|
537
|
+
font-size: 12px;
|
|
538
|
+
font-family: inherit;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.pagination button:hover { border-color: #555; color: #e5e5e5; }
|
|
542
|
+
.pagination button:disabled { opacity: 0.3; cursor: default; }
|
|
543
|
+
|
|
544
|
+
/* ─── Empty State ─────────────────────────────────────────── */
|
|
545
|
+
|
|
546
|
+
.empty {
|
|
547
|
+
text-align: center;
|
|
548
|
+
padding: 60px 20px;
|
|
549
|
+
color: #444;
|
|
550
|
+
font-size: 14px;
|
|
551
|
+
line-height: 1.8;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* ─── Responsive ──────────────────────────────────────────── */
|
|
555
|
+
|
|
556
|
+
@media (max-width: 700px) {
|
|
557
|
+
.app { grid-template-columns: 1fr; }
|
|
558
|
+
.sidebar {
|
|
559
|
+
display: flex;
|
|
560
|
+
overflow-x: auto;
|
|
561
|
+
border-right: none;
|
|
562
|
+
border-bottom: 1px solid #222;
|
|
563
|
+
padding: 8px 12px;
|
|
564
|
+
gap: 4px;
|
|
565
|
+
}
|
|
566
|
+
.sidebar-section { display: none; }
|
|
567
|
+
.sidebar .db-info { display: none; }
|
|
568
|
+
.sidebar a {
|
|
569
|
+
padding: 4px 10px;
|
|
570
|
+
border-left: none;
|
|
571
|
+
border-radius: 4px;
|
|
572
|
+
white-space: nowrap;
|
|
573
|
+
}
|
|
574
|
+
.sidebar a.indent { padding-left: 10px; }
|
|
575
|
+
.sidebar a.active { background: #222; border-left: none; }
|
|
576
|
+
.search-box { width: 180px; }
|
|
577
|
+
.main { padding: 12px; }
|
|
578
|
+
.connection-pill { display: none; }
|
|
579
|
+
}
|
|
580
|
+
</style>
|
|
581
|
+
</head>
|
|
582
|
+
<body>
|
|
583
|
+
<div class="app">
|
|
584
|
+
<header>
|
|
585
|
+
<h1><span>context</span>-mcp</h1>
|
|
586
|
+
<div class="header-right">
|
|
587
|
+
<div class="connection-pill" id="connection-pill" title="Click for settings">
|
|
588
|
+
<span class="connection-dot" id="connection-dot"></span>
|
|
589
|
+
<span id="connection-label">connecting...</span>
|
|
590
|
+
</div>
|
|
591
|
+
<input type="text" class="search-box" id="search" placeholder="Search vault...">
|
|
592
|
+
</div>
|
|
593
|
+
</header>
|
|
594
|
+
|
|
595
|
+
<nav class="sidebar" id="sidebar">
|
|
596
|
+
<!-- Fully built by JS from /api/discover -->
|
|
597
|
+
</nav>
|
|
598
|
+
|
|
599
|
+
<main class="main" id="main">
|
|
600
|
+
<div class="empty">Loading...</div>
|
|
601
|
+
</main>
|
|
602
|
+
</div>
|
|
603
|
+
|
|
604
|
+
<script src="https://cdn.jsdelivr.net/npm/marked@15/marked.min.js"></script>
|
|
605
|
+
<script>
|
|
606
|
+
/* biome-ignore lint: temporarily disabled */
|
|
607
|
+
(function() {
|
|
608
|
+
// ─── State ──────────────────────────────────────────────────
|
|
609
|
+
|
|
610
|
+
let discovery = null; // Full discovery response
|
|
611
|
+
let connectionInfo = null;
|
|
612
|
+
|
|
613
|
+
// Current view state
|
|
614
|
+
let view = { type: "table", table: "", groupCol: "", groupVal: "", query: "", offset: 0 };
|
|
615
|
+
const LIMIT = 50;
|
|
616
|
+
|
|
617
|
+
// ─── API ────────────────────────────────────────────────────
|
|
618
|
+
|
|
619
|
+
async function api(path, opts) {
|
|
620
|
+
const res = await fetch(path, opts);
|
|
621
|
+
if (!res.ok) throw new Error(`API ${res.status}`);
|
|
622
|
+
return res.json();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ─── Utilities ──────────────────────────────────────────────
|
|
626
|
+
|
|
627
|
+
function esc(s) {
|
|
628
|
+
if (s == null) return "";
|
|
629
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function tryParse(s) {
|
|
633
|
+
if (!s || typeof s !== "string") return s;
|
|
634
|
+
try { return JSON.parse(s); } catch { return s; }
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function strColor(str) {
|
|
638
|
+
let hash = 0;
|
|
639
|
+
for (let i = 0; i < str.length; i++) hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
640
|
+
const h = Math.abs(hash) % 360;
|
|
641
|
+
return `hsl(${h}, 50%, 35%)`;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function fmtDate(d) {
|
|
645
|
+
if (!d) return "";
|
|
646
|
+
return String(d).slice(0, 10);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function fmtSize(bytes) {
|
|
650
|
+
if (!bytes) return "";
|
|
651
|
+
if (bytes < 1024) return bytes + "B";
|
|
652
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + "KB";
|
|
653
|
+
return (bytes / (1024 * 1024)).toFixed(1) + "MB";
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function shortPath(p) {
|
|
657
|
+
if (!p) return "";
|
|
658
|
+
const home = p.match(/^\/Users\/[^/]+|^\/home\/[^/]+/);
|
|
659
|
+
if (home) return p.replace(home[0], "~");
|
|
660
|
+
return p;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function truncate(s, n) {
|
|
664
|
+
if (!s) return "";
|
|
665
|
+
return s.length > n ? s.slice(0, n) + "..." : s;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ─── Column Role Detection ─────────────────────────────────
|
|
669
|
+
// Given a list of column definitions, figure out which columns serve
|
|
670
|
+
// as title, body, badge, tags, date, etc. — no hardcoded table names.
|
|
671
|
+
|
|
672
|
+
const TITLE_NAMES = ["title", "name", "subject", "label", "heading", "summary"];
|
|
673
|
+
const BODY_NAMES = ["body", "content", "text", "description", "rationale", "message", "note", "notes"];
|
|
674
|
+
const BADGE_NAMES = ["kind", "type", "category", "status", "level", "group", "role"];
|
|
675
|
+
const TAG_NAMES = ["tags", "labels", "keywords", "categories"];
|
|
676
|
+
const DATE_NAMES = ["created_at", "updated_at", "date", "timestamp", "created", "modified", "time"];
|
|
677
|
+
const PATH_NAMES = ["file_path", "path", "filepath", "url", "uri", "link"];
|
|
678
|
+
const ID_NAMES = ["id", "uuid", "uid", "rowid", "pk"];
|
|
679
|
+
const SKIP_IN_META = new Set([...ID_NAMES, "rowid", "embedding", "rank"]);
|
|
680
|
+
|
|
681
|
+
function detectRoles(columns) {
|
|
682
|
+
const names = columns.map((c) => c.name.toLowerCase());
|
|
683
|
+
const find = (candidates) => {
|
|
684
|
+
for (const cand of candidates) {
|
|
685
|
+
const idx = names.indexOf(cand);
|
|
686
|
+
if (idx !== -1) return columns[idx].name;
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
};
|
|
690
|
+
return {
|
|
691
|
+
title: find(TITLE_NAMES),
|
|
692
|
+
body: find(BODY_NAMES),
|
|
693
|
+
badge: find(BADGE_NAMES),
|
|
694
|
+
tags: find(TAG_NAMES),
|
|
695
|
+
date: find(DATE_NAMES),
|
|
696
|
+
path: find(PATH_NAMES),
|
|
697
|
+
id: find(ID_NAMES),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ─── Update Connection Pill ────────────────────────────────
|
|
702
|
+
|
|
703
|
+
function updateConnectionPill(info) {
|
|
704
|
+
connectionInfo = info;
|
|
705
|
+
const dot = document.getElementById("connection-dot");
|
|
706
|
+
const label = document.getElementById("connection-label");
|
|
707
|
+
|
|
708
|
+
if (info && info.dbExists) {
|
|
709
|
+
dot.className = "connection-dot";
|
|
710
|
+
label.textContent = shortPath(info.dbPath);
|
|
711
|
+
label.title = `DB: ${info.dbPath}\nVault: ${info.vaultDir}`;
|
|
712
|
+
} else {
|
|
713
|
+
dot.className = "connection-dot disconnected";
|
|
714
|
+
label.textContent = info ? "DB not found" : "disconnected";
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ─── Build Sidebar from Discovery ──────────────────────────
|
|
719
|
+
|
|
720
|
+
function buildSidebar() {
|
|
721
|
+
const sidebar = document.getElementById("sidebar");
|
|
722
|
+
sidebar.innerHTML = "";
|
|
723
|
+
|
|
724
|
+
if (!discovery) {
|
|
725
|
+
sidebar.innerHTML = '<div class="db-info">Loading...</div>';
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const { tables, tableGroups, directories, vaultDirName, dbSize, connection } = discovery;
|
|
730
|
+
|
|
731
|
+
// Filter to "real" data tables (skip internal/index tables)
|
|
732
|
+
const internalPrefixes = ["sqlite_", "vault_fts", "vault_vec"];
|
|
733
|
+
const isInternal = (name) => {
|
|
734
|
+
if (name.endsWith("_fts") || name.endsWith("_vec") || name.endsWith("_idx")) return true;
|
|
735
|
+
for (const p of internalPrefixes) if (name.startsWith(p)) return true;
|
|
736
|
+
return false;
|
|
737
|
+
};
|
|
738
|
+
const dataTables = tables.filter((t) => !isInternal(t.name) && t.count > 0);
|
|
739
|
+
const emptyTables = tables.filter((t) => !isInternal(t.name) && t.count === 0);
|
|
740
|
+
|
|
741
|
+
// ── Database section ──
|
|
742
|
+
if (dataTables.length > 0) {
|
|
743
|
+
addSection("Database");
|
|
744
|
+
|
|
745
|
+
for (const t of dataTables) {
|
|
746
|
+
const groups = tableGroups[t.name];
|
|
747
|
+
|
|
748
|
+
// Table-level link: show all rows
|
|
749
|
+
addLink(t.name, t.count, () => {
|
|
750
|
+
view = { type: "table", table: t.name, groupCol: "", groupVal: "", query: "", offset: 0 };
|
|
751
|
+
loadTableData();
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// If this table has a group column, show sub-links
|
|
755
|
+
if (groups && groups.groups.length > 1) {
|
|
756
|
+
for (const g of groups.groups) {
|
|
757
|
+
addLink(g.value || "(empty)", g.count, () => {
|
|
758
|
+
view = { type: "table", table: t.name, groupCol: groups.column, groupVal: g.value, query: "", offset: 0 };
|
|
759
|
+
loadTableData();
|
|
760
|
+
}, true);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ── Files section ──
|
|
767
|
+
if (connection.vaultDirExists && directories.length > 0) {
|
|
768
|
+
addSection("Files");
|
|
769
|
+
addLink(vaultDirName || "root", null, () => {
|
|
770
|
+
view = { type: "files", browsePath: "" };
|
|
771
|
+
loadFiles("");
|
|
772
|
+
});
|
|
773
|
+
for (const d of directories) {
|
|
774
|
+
if (d.type === "directory") {
|
|
775
|
+
addLink(d.name, d.fileCount || null, () => {
|
|
776
|
+
view = { type: "files", browsePath: d.name };
|
|
777
|
+
loadFiles(d.name);
|
|
778
|
+
}, true);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ── System section ──
|
|
784
|
+
addSection("System");
|
|
785
|
+
if (tables.length > 0) {
|
|
786
|
+
addLink("Schema", tables.length, () => {
|
|
787
|
+
view = { type: "schema" };
|
|
788
|
+
loadSchema();
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
addLink("Settings", null, () => {
|
|
792
|
+
view = { type: "settings" };
|
|
793
|
+
loadSettings();
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// ── DB info footer ──
|
|
797
|
+
const info = document.createElement("div");
|
|
798
|
+
info.className = "db-info";
|
|
799
|
+
info.innerHTML = `
|
|
800
|
+
<div>DB: ${esc(dbSize)}</div>
|
|
801
|
+
<div style="margin-top:4px;font-size:10px;color:#333">${esc(shortPath(connection.vaultDir))}</div>
|
|
802
|
+
`;
|
|
803
|
+
sidebar.appendChild(info);
|
|
804
|
+
|
|
805
|
+
// ── Helpers ──
|
|
806
|
+
function addSection(label) {
|
|
807
|
+
const el = document.createElement("div");
|
|
808
|
+
el.className = "sidebar-section";
|
|
809
|
+
el.textContent = label;
|
|
810
|
+
sidebar.appendChild(el);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function addLink(label, count, onClick, indent) {
|
|
814
|
+
const a = document.createElement("a");
|
|
815
|
+
a.href = "#";
|
|
816
|
+
a.className = indent ? "indent" : "";
|
|
817
|
+
a.innerHTML = `${esc(label)}${count != null ? ` <span class="count">${count}</span>` : ""}`;
|
|
818
|
+
a.addEventListener("click", (e) => {
|
|
819
|
+
e.preventDefault();
|
|
820
|
+
setActiveNav(a);
|
|
821
|
+
onClick();
|
|
822
|
+
});
|
|
823
|
+
sidebar.appendChild(a);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function setActiveNav(el) {
|
|
828
|
+
document.querySelectorAll(".sidebar a").forEach((a) => a.classList.remove("active"));
|
|
829
|
+
if (el) el.classList.add("active");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// ─── Render Table Data (generic) ───────────────────────────
|
|
833
|
+
|
|
834
|
+
async function loadTableData() {
|
|
835
|
+
const { table, groupCol, groupVal, query, offset } = view;
|
|
836
|
+
const params = new URLSearchParams({ table, limit: LIMIT, offset: offset || 0 });
|
|
837
|
+
if (groupCol && groupVal) {
|
|
838
|
+
params.set("groupCol", groupCol);
|
|
839
|
+
params.set("groupVal", groupVal);
|
|
840
|
+
}
|
|
841
|
+
if (query) params.set("q", query);
|
|
842
|
+
|
|
843
|
+
try {
|
|
844
|
+
const data = await api(`/api/table-data?${params}`);
|
|
845
|
+
const main = document.getElementById("main");
|
|
846
|
+
const roles = detectRoles(data.columns);
|
|
847
|
+
|
|
848
|
+
if (!data.rows.length) {
|
|
849
|
+
main.innerHTML = `<div class="empty">${query ? "No rows matching your search" : "No data"}</div>`;
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Header
|
|
854
|
+
const label = groupVal ? `${esc(table)} / ${esc(groupVal)}` : esc(table);
|
|
855
|
+
const searchNote = query ? ` matching "${esc(query)}"` : "";
|
|
856
|
+
const totalPages = Math.ceil(data.total / LIMIT);
|
|
857
|
+
const currentPage = Math.floor((offset || 0) / LIMIT) + 1;
|
|
858
|
+
let html = `<div class="main-header">
|
|
859
|
+
<span>${data.total} row${data.total === 1 ? "" : "s"} in ${label}${searchNote}</span>
|
|
860
|
+
${totalPages > 1 ? `<span>Page ${currentPage} of ${totalPages}</span>` : ""}
|
|
861
|
+
</div>`;
|
|
862
|
+
|
|
863
|
+
// Render each row as a card
|
|
864
|
+
html += data.rows.map((row) => renderRowCard(row, data.columns, roles)).join("");
|
|
865
|
+
|
|
866
|
+
// Pagination
|
|
867
|
+
if (totalPages > 1) {
|
|
868
|
+
html += `<div class="pagination">
|
|
869
|
+
<button id="prev-page" ${currentPage <= 1 ? "disabled" : ""}>Prev</button>
|
|
870
|
+
<button id="next-page" ${currentPage >= totalPages ? "disabled" : ""}>Next</button>
|
|
871
|
+
</div>`;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
main.innerHTML = html;
|
|
875
|
+
|
|
876
|
+
// Bind expand/collapse
|
|
877
|
+
main.querySelectorAll(".row-card").forEach((card) => {
|
|
878
|
+
card.addEventListener("click", () => card.classList.toggle("expanded"));
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
// Bind pagination
|
|
882
|
+
const prev = document.getElementById("prev-page");
|
|
883
|
+
const next = document.getElementById("next-page");
|
|
884
|
+
if (prev) prev.addEventListener("click", () => { view.offset = (view.offset || 0) - LIMIT; loadTableData(); });
|
|
885
|
+
if (next) next.addEventListener("click", () => { view.offset = (view.offset || 0) + LIMIT; loadTableData(); });
|
|
886
|
+
} catch (e) {
|
|
887
|
+
document.getElementById("main").innerHTML = `<div class="empty">Failed to load data: ${esc(e.message)}</div>`;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function renderRowCard(row, columns, roles) {
|
|
892
|
+
// Title: use detected title column, or first non-empty text-ish value
|
|
893
|
+
let titleText = "";
|
|
894
|
+
if (roles.title && row[roles.title]) {
|
|
895
|
+
titleText = truncate(String(row[roles.title]), 150);
|
|
896
|
+
} else if (roles.body && row[roles.body]) {
|
|
897
|
+
titleText = truncate(String(row[roles.body]), 120);
|
|
898
|
+
} else {
|
|
899
|
+
// Fallback: first non-null non-id value
|
|
900
|
+
for (const col of columns) {
|
|
901
|
+
if (SKIP_IN_META.has(col.name.toLowerCase())) continue;
|
|
902
|
+
if (row[col.name] != null && String(row[col.name]).length > 0) {
|
|
903
|
+
titleText = truncate(String(row[col.name]), 120);
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (!titleText) titleText = "(empty)";
|
|
909
|
+
|
|
910
|
+
// Badge: group/type column
|
|
911
|
+
let badgeHtml = "";
|
|
912
|
+
if (roles.badge && row[roles.badge]) {
|
|
913
|
+
const val = String(row[roles.badge]);
|
|
914
|
+
badgeHtml = `<span class="badge" style="background:${strColor(val)}">${esc(val)}</span>`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Tags
|
|
918
|
+
let tagsHtml = "";
|
|
919
|
+
if (roles.tags && row[roles.tags]) {
|
|
920
|
+
const parsed = tryParse(row[roles.tags]);
|
|
921
|
+
const arr = Array.isArray(parsed) ? parsed : (typeof parsed === "string" ? parsed.split(",").map((s) => s.trim()) : []);
|
|
922
|
+
tagsHtml = arr.filter(Boolean).map((t) => `<span class="tag">${esc(String(t))}</span>`).join("");
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Date
|
|
926
|
+
let dateHtml = "";
|
|
927
|
+
if (roles.date && row[roles.date]) {
|
|
928
|
+
dateHtml = `<span>${esc(fmtDate(row[roles.date]))}</span>`;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Body for expanded detail (markdown rendered)
|
|
932
|
+
let bodyHtml = "";
|
|
933
|
+
if (roles.body && row[roles.body]) {
|
|
934
|
+
const rawText = String(row[roles.body]);
|
|
935
|
+
const rendered = typeof marked !== "undefined" ? marked.parse(rawText) : esc(rawText);
|
|
936
|
+
bodyHtml = `<button class="raw-toggle" onclick="event.stopPropagation(); this.parentElement.querySelector('.row-body').classList.toggle('raw'); this.textContent = this.textContent === 'Raw' ? 'Rendered' : 'Raw'">Raw</button>
|
|
937
|
+
<div class="row-body">${rendered}</div>`;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// All other fields as metadata
|
|
941
|
+
const shownKeys = new Set([roles.title, roles.body, roles.badge, roles.tags, roles.date].filter(Boolean));
|
|
942
|
+
let metaHtml = "";
|
|
943
|
+
for (const col of columns) {
|
|
944
|
+
const key = col.name;
|
|
945
|
+
if (shownKeys.has(key)) continue;
|
|
946
|
+
if (SKIP_IN_META.has(key.toLowerCase())) continue;
|
|
947
|
+
if (row[key] == null || String(row[key]).length === 0) continue;
|
|
948
|
+
|
|
949
|
+
let val = row[key];
|
|
950
|
+
// Try to display parsed JSON objects nicely
|
|
951
|
+
const parsed = tryParse(val);
|
|
952
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
953
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
954
|
+
if (v != null) metaHtml += `<div class="row-field"><strong>${esc(k)}:</strong> ${esc(String(v))}</div>`;
|
|
955
|
+
}
|
|
956
|
+
} else {
|
|
957
|
+
metaHtml += `<div class="row-field"><strong>${esc(key)}:</strong> ${esc(truncate(String(val), 500))}</div>`;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return `<div class="row-card">
|
|
962
|
+
<div class="row-title">${esc(titleText)}</div>
|
|
963
|
+
<div class="row-meta">${badgeHtml}${tagsHtml}${dateHtml}</div>
|
|
964
|
+
<div class="row-detail">${bodyHtml}${metaHtml}</div>
|
|
965
|
+
</div>`;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// ─── Schema View ───────────────────────────────────────────
|
|
969
|
+
|
|
970
|
+
function loadSchema() {
|
|
971
|
+
const main = document.getElementById("main");
|
|
972
|
+
if (!discovery || !discovery.tables.length) {
|
|
973
|
+
main.innerHTML = `<div class="empty">No tables found in database</div>`;
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const internalPrefixes = ["sqlite_", "vault_fts", "vault_vec"];
|
|
978
|
+
const isInternal = (name) => {
|
|
979
|
+
if (name.endsWith("_fts") || name.endsWith("_vec") || name.endsWith("_idx")) return true;
|
|
980
|
+
for (const p of internalPrefixes) if (name.startsWith(p)) return true;
|
|
981
|
+
return false;
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
const userTables = discovery.tables.filter((t) => !isInternal(t.name));
|
|
985
|
+
const internalTables = discovery.tables.filter((t) => isInternal(t.name));
|
|
986
|
+
|
|
987
|
+
const renderCard = (t) => {
|
|
988
|
+
const cols = t.columns.map((c) =>
|
|
989
|
+
`<span class="table-col">${esc(c.name)}<span class="col-type">${esc(c.type || "")}</span></span>`
|
|
990
|
+
).join("");
|
|
991
|
+
return `<div class="table-card" data-table="${esc(t.name)}">
|
|
992
|
+
<div class="table-name">
|
|
993
|
+
${esc(t.name)}
|
|
994
|
+
<span class="badge" style="background:#1a2a3a;color:#6ad">${esc(t.type)}</span>
|
|
995
|
+
<span style="font-size:12px;color:#666;font-weight:normal">${t.count} rows</span>
|
|
996
|
+
</div>
|
|
997
|
+
<div class="table-columns">${cols}</div>
|
|
998
|
+
</div>`;
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
const headerText = `${userTables.length} table${userTables.length !== 1 ? "s" : ""}` +
|
|
1002
|
+
(internalTables.length ? ` · ${internalTables.length} internal` : "");
|
|
1003
|
+
const header = `<div class="main-header"><span>${headerText}</span></div>`;
|
|
1004
|
+
|
|
1005
|
+
const userSection = `<div class="schema-section-label">Tables</div>` +
|
|
1006
|
+
userTables.map(renderCard).join("");
|
|
1007
|
+
|
|
1008
|
+
const internalSection = internalTables.length ? (
|
|
1009
|
+
`<div class="schema-section-label">` +
|
|
1010
|
+
`<span class="internal-toggle" id="internal-toggle">` +
|
|
1011
|
+
`<span class="arrow">▶</span> Internal (${internalTables.length})` +
|
|
1012
|
+
`</span>` +
|
|
1013
|
+
`</div>` +
|
|
1014
|
+
`<div class="internal-tables" id="internal-tables">` +
|
|
1015
|
+
internalTables.map(renderCard).join("") +
|
|
1016
|
+
`</div>`
|
|
1017
|
+
) : "";
|
|
1018
|
+
|
|
1019
|
+
main.innerHTML = header + userSection + internalSection;
|
|
1020
|
+
|
|
1021
|
+
// Toggle internal section
|
|
1022
|
+
const toggle = document.getElementById("internal-toggle");
|
|
1023
|
+
const container = document.getElementById("internal-tables");
|
|
1024
|
+
if (toggle && container) {
|
|
1025
|
+
toggle.addEventListener("click", () => {
|
|
1026
|
+
container.classList.toggle("open");
|
|
1027
|
+
toggle.querySelector(".arrow").classList.toggle("open");
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Click to navigate to table data
|
|
1032
|
+
main.querySelectorAll(".table-card").forEach((card) => {
|
|
1033
|
+
card.addEventListener("click", () => {
|
|
1034
|
+
const tableName = card.dataset.table;
|
|
1035
|
+
view = { type: "table", table: tableName, groupCol: "", groupVal: "", query: "", offset: 0 };
|
|
1036
|
+
buildSidebar();
|
|
1037
|
+
loadTableData();
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// ─── File Browser ──────────────────────────────────────────
|
|
1043
|
+
|
|
1044
|
+
async function loadFiles(browsePath) {
|
|
1045
|
+
view.browsePath = browsePath || "";
|
|
1046
|
+
try {
|
|
1047
|
+
const params = new URLSearchParams();
|
|
1048
|
+
if (view.browsePath) params.set("path", view.browsePath);
|
|
1049
|
+
|
|
1050
|
+
const data = await api(`/api/browse?${params}`);
|
|
1051
|
+
const main = document.getElementById("main");
|
|
1052
|
+
const rootLabel = data.baseName || "files";
|
|
1053
|
+
|
|
1054
|
+
if (!data.exists) {
|
|
1055
|
+
main.innerHTML = `<div class="empty">Directory not found<br><span style="font-size:12px;color:#555;margin-top:8px;display:block">${esc(data.path)}</span></div>`;
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Breadcrumb
|
|
1060
|
+
let breadcrumb = `<div class="breadcrumb">`;
|
|
1061
|
+
breadcrumb += `<a onclick="window.__browse('')">${esc(rootLabel)}</a>`;
|
|
1062
|
+
if (view.browsePath) {
|
|
1063
|
+
const parts = view.browsePath.split("/").filter(Boolean);
|
|
1064
|
+
let accumulated = "";
|
|
1065
|
+
for (const part of parts) {
|
|
1066
|
+
accumulated += (accumulated ? "/" : "") + part;
|
|
1067
|
+
const p = accumulated;
|
|
1068
|
+
breadcrumb += `<span class="sep">/</span><a onclick="window.__browse('${esc(p)}')">${esc(part)}</a>`;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
breadcrumb += `</div>`;
|
|
1072
|
+
|
|
1073
|
+
if (!data.items.length) {
|
|
1074
|
+
main.innerHTML = breadcrumb + `<div class="empty" style="padding:30px">Empty directory</div>`;
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const items = data.items.map((item) => {
|
|
1079
|
+
if (item.type === "directory") {
|
|
1080
|
+
return `<div class="file-item" onclick="window.__browse('${esc(item.path)}')">
|
|
1081
|
+
<span class="file-icon" style="color:#6ad">📁</span>
|
|
1082
|
+
<span class="file-name">${esc(item.name)}</span>
|
|
1083
|
+
<span class="file-meta">${item.childCount} items</span>
|
|
1084
|
+
</div>`;
|
|
1085
|
+
}
|
|
1086
|
+
return `<div class="file-item" onclick="window.__preview('${esc(item.path)}')">
|
|
1087
|
+
<span class="file-icon" style="color:#888">📄</span>
|
|
1088
|
+
<span class="file-name">${esc(item.name)}</span>
|
|
1089
|
+
<span class="file-meta">${fmtSize(item.size)}</span>
|
|
1090
|
+
</div>`;
|
|
1091
|
+
}).join("");
|
|
1092
|
+
|
|
1093
|
+
main.innerHTML = breadcrumb + items;
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
document.getElementById("main").innerHTML = `<div class="empty">Failed to browse: ${esc(e.message)}</div>`;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
async function previewFile(path) {
|
|
1100
|
+
try {
|
|
1101
|
+
const data = await api(`/api/file?path=${encodeURIComponent(path)}`);
|
|
1102
|
+
const main = document.getElementById("main");
|
|
1103
|
+
const rootLabel = data.baseName || "files";
|
|
1104
|
+
|
|
1105
|
+
let breadcrumb = `<div class="breadcrumb">`;
|
|
1106
|
+
breadcrumb += `<a onclick="window.__browse('')">${esc(rootLabel)}</a>`;
|
|
1107
|
+
const parts = path.split("/").filter(Boolean);
|
|
1108
|
+
let accumulated = "";
|
|
1109
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1110
|
+
accumulated += (accumulated ? "/" : "") + parts[i];
|
|
1111
|
+
const p = accumulated;
|
|
1112
|
+
breadcrumb += `<span class="sep">/</span><a onclick="window.__browse('${esc(p)}')">${esc(parts[i])}</a>`;
|
|
1113
|
+
}
|
|
1114
|
+
breadcrumb += `<span class="sep">/</span><span style="color:#e5e5e5">${esc(parts[parts.length - 1])}</span>`;
|
|
1115
|
+
breadcrumb += `</div>`;
|
|
1116
|
+
|
|
1117
|
+
const meta = `<div style="font-size:11px;color:#555;margin-bottom:12px;display:flex;gap:12px">
|
|
1118
|
+
<span>${fmtSize(data.size)}</span>
|
|
1119
|
+
<span>Modified: ${fmtDate(data.modified)}</span>
|
|
1120
|
+
</div>`;
|
|
1121
|
+
|
|
1122
|
+
const rendered = typeof marked !== "undefined" ? marked.parse(data.content) : esc(data.content);
|
|
1123
|
+
main.innerHTML = breadcrumb + meta +
|
|
1124
|
+
`<button class="raw-toggle" onclick="var fp=document.getElementById('file-preview'); fp.classList.toggle('rendered'); fp.classList.toggle('raw'); if(fp.classList.contains('raw')){fp.innerHTML=window.__fileRawHtml}else{fp.innerHTML=window.__fileRenderedHtml}; this.textContent=fp.classList.contains('raw')?'Rendered':'Raw'">Raw</button>` +
|
|
1125
|
+
`<div class="file-preview rendered" id="file-preview">${rendered}</div>`;
|
|
1126
|
+
window.__fileRawHtml = esc(data.content);
|
|
1127
|
+
window.__fileRenderedHtml = rendered;
|
|
1128
|
+
} catch (e) {
|
|
1129
|
+
document.getElementById("main").innerHTML = `<div class="empty">Failed to load file: ${esc(e.message)}</div>`;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
window.__browse = (p) => {
|
|
1134
|
+
view = { type: "files", browsePath: p };
|
|
1135
|
+
loadFiles(p);
|
|
1136
|
+
};
|
|
1137
|
+
window.__preview = previewFile;
|
|
1138
|
+
|
|
1139
|
+
// ─── Settings View ─────────────────────────────────────────
|
|
1140
|
+
|
|
1141
|
+
async function loadSettings() {
|
|
1142
|
+
try {
|
|
1143
|
+
const data = await api("/api/config");
|
|
1144
|
+
const main = document.getElementById("main");
|
|
1145
|
+
const active = data.active || {};
|
|
1146
|
+
const saved = data.config || {};
|
|
1147
|
+
|
|
1148
|
+
main.innerHTML = `
|
|
1149
|
+
<div class="main-header"><span>Settings</span></div>
|
|
1150
|
+
<div class="settings-section">
|
|
1151
|
+
<h2>Paths</h2>
|
|
1152
|
+
<div class="settings-field">
|
|
1153
|
+
<label>Vault Directory</label>
|
|
1154
|
+
<input type="text" id="cfg-vaultDir" value="${esc(active.vaultDir || saved.vaultDir || "")}" placeholder="/path/to/vault">
|
|
1155
|
+
<div class="field-hint">Root directory for markdown knowledge files. Any folder structure is supported.</div>
|
|
1156
|
+
<div class="field-status" id="status-vaultDir"></div>
|
|
1157
|
+
</div>
|
|
1158
|
+
<div class="settings-field">
|
|
1159
|
+
<label>Database Path</label>
|
|
1160
|
+
<input type="text" id="cfg-dbPath" value="${esc(active.dbPath || saved.dbPath || "")}" placeholder="/path/to/vault.db">
|
|
1161
|
+
<div class="field-hint">Any SQLite database. Tables and columns are auto-detected.</div>
|
|
1162
|
+
<div class="field-status" id="status-dbPath"></div>
|
|
1163
|
+
</div>
|
|
1164
|
+
<div class="settings-field">
|
|
1165
|
+
<label>Data Directory</label>
|
|
1166
|
+
<input type="text" id="cfg-dataDir" value="${esc(active.dataDir || saved.dataDir || "")}" placeholder="/path/to/.context-mcp">
|
|
1167
|
+
<div class="field-hint">Directory for database and configuration storage.</div>
|
|
1168
|
+
</div>
|
|
1169
|
+
<div class="settings-field">
|
|
1170
|
+
<label>Dev Directory</label>
|
|
1171
|
+
<input type="text" id="cfg-devDir" value="${esc(active.devDir || saved.devDir || "")}" placeholder="/path/to/dev">
|
|
1172
|
+
<div class="field-hint">Root development directory used for project discovery.</div>
|
|
1173
|
+
</div>
|
|
1174
|
+
<div class="settings-actions">
|
|
1175
|
+
<button class="btn btn-primary" id="save-config">Save & Reconnect</button>
|
|
1176
|
+
</div>
|
|
1177
|
+
<div class="settings-toast" id="settings-toast"></div>
|
|
1178
|
+
</div>
|
|
1179
|
+
|
|
1180
|
+
<div class="settings-section">
|
|
1181
|
+
<h2>Active Connection</h2>
|
|
1182
|
+
<div style="font-size:13px;color:#888;line-height:1.8">
|
|
1183
|
+
<div><strong style="color:#aaa">Database:</strong> ${esc(active.dbPath || "not set")}</div>
|
|
1184
|
+
<div><strong style="color:#aaa">Vault:</strong> ${esc(active.vaultDir || "not set")}</div>
|
|
1185
|
+
<div><strong style="color:#aaa">Data:</strong> ${esc(active.dataDir || "not set")}</div>
|
|
1186
|
+
<div><strong style="color:#aaa">Dev:</strong> ${esc(active.devDir || "not set")}</div>
|
|
1187
|
+
<div style="margin-top:8px;font-size:11px;color:#555">Config file: ${esc(data.path || "")}</div>
|
|
1188
|
+
</div>
|
|
1189
|
+
</div>
|
|
1190
|
+
`;
|
|
1191
|
+
|
|
1192
|
+
// Path validation
|
|
1193
|
+
if (connectionInfo) {
|
|
1194
|
+
const vd = document.getElementById("status-vaultDir");
|
|
1195
|
+
const dp = document.getElementById("status-dbPath");
|
|
1196
|
+
vd.className = connectionInfo.vaultDirExists ? "field-status ok" : "field-status warn";
|
|
1197
|
+
vd.textContent = connectionInfo.vaultDirExists ? "Directory exists" : "Directory not found";
|
|
1198
|
+
dp.className = connectionInfo.dbExists ? "field-status ok" : "field-status warn";
|
|
1199
|
+
dp.textContent = connectionInfo.dbExists ? "Database connected" : "Database will be created";
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Save
|
|
1203
|
+
document.getElementById("save-config").addEventListener("click", async () => {
|
|
1204
|
+
const toast = document.getElementById("settings-toast");
|
|
1205
|
+
try {
|
|
1206
|
+
const newConfig = {};
|
|
1207
|
+
for (const key of ["vaultDir", "dbPath", "dataDir", "devDir"]) {
|
|
1208
|
+
const val = document.getElementById("cfg-" + key).value.trim();
|
|
1209
|
+
if (val) newConfig[key] = val;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
await api("/api/config", {
|
|
1213
|
+
method: "PUT",
|
|
1214
|
+
headers: { "Content-Type": "application/json" },
|
|
1215
|
+
body: JSON.stringify(newConfig),
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
toast.className = "settings-toast success";
|
|
1219
|
+
toast.textContent = "Saved. Reloading...";
|
|
1220
|
+
|
|
1221
|
+
// Full reload
|
|
1222
|
+
await init();
|
|
1223
|
+
setTimeout(() => loadSettings(), 300);
|
|
1224
|
+
} catch (e) {
|
|
1225
|
+
toast.className = "settings-toast error";
|
|
1226
|
+
toast.textContent = "Failed: " + e.message;
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
} catch (e) {
|
|
1230
|
+
document.getElementById("main").innerHTML = `<div class="empty">Failed to load settings: ${esc(e.message)}</div>`;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// ─── Search ────────────────────────────────────────────────
|
|
1235
|
+
|
|
1236
|
+
// ─── Semantic Search ──────────────────────────────────────
|
|
1237
|
+
|
|
1238
|
+
async function loadSearchResults(query) {
|
|
1239
|
+
try {
|
|
1240
|
+
const data = await api(`/api/search?q=${encodeURIComponent(query)}`);
|
|
1241
|
+
const main = document.getElementById("main");
|
|
1242
|
+
|
|
1243
|
+
if (!data.results || !data.results.length) {
|
|
1244
|
+
main.innerHTML = `<div class="empty">No results for "${esc(query)}"</div>`;
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
let html = `<div class="main-header"><span>${data.results.length} result${data.results.length === 1 ? "" : "s"} for "${esc(query)}"</span></div>`;
|
|
1249
|
+
|
|
1250
|
+
html += data.results.map((row) => {
|
|
1251
|
+
const titleText = row.title ? truncate(String(row.title), 150) : truncate(String(row.body || ""), 120);
|
|
1252
|
+
const badgeHtml = row.kind ? `<span class="badge" style="background:${strColor(row.kind)}">${esc(row.kind)}</span>` : "";
|
|
1253
|
+
const scoreHtml = `<span style="font-size:11px;color:#555">score: ${row.score.toFixed(3)}</span>`;
|
|
1254
|
+
|
|
1255
|
+
let tagsHtml = "";
|
|
1256
|
+
if (row.tags) {
|
|
1257
|
+
const parsed = tryParse(row.tags);
|
|
1258
|
+
const arr = Array.isArray(parsed) ? parsed : (typeof parsed === "string" ? parsed.split(",").map((s) => s.trim()) : []);
|
|
1259
|
+
tagsHtml = arr.filter(Boolean).map((t) => `<span class="tag">${esc(String(t))}</span>`).join("");
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const dateHtml = row.created_at ? `<span>${esc(fmtDate(row.created_at))}</span>` : "";
|
|
1263
|
+
|
|
1264
|
+
let bodyHtml = "";
|
|
1265
|
+
if (row.body) {
|
|
1266
|
+
const rawText = String(row.body);
|
|
1267
|
+
const rendered = typeof marked !== "undefined" ? marked.parse(rawText) : esc(rawText);
|
|
1268
|
+
bodyHtml = `<button class="raw-toggle" onclick="event.stopPropagation(); this.parentElement.querySelector('.row-body').classList.toggle('raw'); this.textContent = this.textContent === 'Raw' ? 'Rendered' : 'Raw'">Raw</button>
|
|
1269
|
+
<div class="row-body">${rendered}</div>`;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
let metaHtml = "";
|
|
1273
|
+
if (row.file_path) metaHtml += `<div class="row-field"><strong>file:</strong> ${esc(String(row.file_path))}</div>`;
|
|
1274
|
+
if (row.meta) {
|
|
1275
|
+
const parsed = tryParse(row.meta);
|
|
1276
|
+
if (parsed && typeof parsed === "object") {
|
|
1277
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
1278
|
+
if (v != null) metaHtml += `<div class="row-field"><strong>${esc(k)}:</strong> ${esc(String(v))}</div>`;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
return `<div class="row-card">
|
|
1284
|
+
<div class="row-title">${esc(titleText || "(empty)")}</div>
|
|
1285
|
+
<div class="row-meta">${badgeHtml}${tagsHtml}${dateHtml}${scoreHtml}</div>
|
|
1286
|
+
<div class="row-detail">${bodyHtml}${metaHtml}</div>
|
|
1287
|
+
</div>`;
|
|
1288
|
+
}).join("");
|
|
1289
|
+
|
|
1290
|
+
main.innerHTML = html;
|
|
1291
|
+
main.querySelectorAll(".row-card").forEach((card) => {
|
|
1292
|
+
card.addEventListener("click", () => card.classList.toggle("expanded"));
|
|
1293
|
+
});
|
|
1294
|
+
} catch (e) {
|
|
1295
|
+
document.getElementById("main").innerHTML = `<div class="empty">Search failed: ${esc(e.message)}</div>`;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
let searchTimer;
|
|
1300
|
+
document.getElementById("search").addEventListener("input", (e) => {
|
|
1301
|
+
clearTimeout(searchTimer);
|
|
1302
|
+
searchTimer = setTimeout(() => {
|
|
1303
|
+
const q = e.target.value.trim();
|
|
1304
|
+
|
|
1305
|
+
if (!q) {
|
|
1306
|
+
// Restore previous view
|
|
1307
|
+
if (view.type === "search" && view.prevView) {
|
|
1308
|
+
view = view.prevView;
|
|
1309
|
+
}
|
|
1310
|
+
if (view.type === "table") loadTableData();
|
|
1311
|
+
else if (view.type === "files") loadFiles(view.browsePath || "");
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
view = { type: "search", query: q, prevView: view.type !== "search" ? { ...view } : view.prevView };
|
|
1316
|
+
setActiveNav(null);
|
|
1317
|
+
loadSearchResults(q);
|
|
1318
|
+
}, 300);
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
// Connection pill → settings
|
|
1322
|
+
document.getElementById("connection-pill").addEventListener("click", () => {
|
|
1323
|
+
view = { type: "settings" };
|
|
1324
|
+
setActiveNav(null);
|
|
1325
|
+
loadSettings();
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
// ─── Init ──────────────────────────────────────────────────
|
|
1329
|
+
|
|
1330
|
+
async function init() {
|
|
1331
|
+
try {
|
|
1332
|
+
discovery = await api("/api/discover");
|
|
1333
|
+
|
|
1334
|
+
if (discovery.connection) {
|
|
1335
|
+
updateConnectionPill(discovery.connection);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
buildSidebar();
|
|
1339
|
+
|
|
1340
|
+
// Auto-select first view: first data table, or files, or settings
|
|
1341
|
+
const dataTables = discovery.tables.filter((t) =>
|
|
1342
|
+
!t.name.endsWith("_fts") && !t.name.endsWith("_vec") && !t.name.endsWith("_idx") && t.count > 0
|
|
1343
|
+
);
|
|
1344
|
+
|
|
1345
|
+
if (dataTables.length > 0) {
|
|
1346
|
+
view = { type: "table", table: dataTables[0].name, groupCol: "", groupVal: "", query: "", offset: 0 };
|
|
1347
|
+
// Activate first sidebar link
|
|
1348
|
+
const firstLink = document.querySelector(".sidebar a:not(.indent)");
|
|
1349
|
+
if (firstLink) setActiveNav(firstLink);
|
|
1350
|
+
loadTableData();
|
|
1351
|
+
} else if (discovery.connection.vaultDirExists) {
|
|
1352
|
+
view = { type: "files", browsePath: "" };
|
|
1353
|
+
loadFiles("");
|
|
1354
|
+
} else {
|
|
1355
|
+
view = { type: "settings" };
|
|
1356
|
+
loadSettings();
|
|
1357
|
+
document.getElementById("main").innerHTML = `<div class="empty">
|
|
1358
|
+
No data found.<br>
|
|
1359
|
+
<span style="font-size:12px;color:#555;margin-top:8px;display:block">
|
|
1360
|
+
Configure your database and knowledge directory in Settings.
|
|
1361
|
+
</span>
|
|
1362
|
+
</div>`;
|
|
1363
|
+
}
|
|
1364
|
+
} catch (e) {
|
|
1365
|
+
updateConnectionPill(null);
|
|
1366
|
+
document.getElementById("main").innerHTML = `<div class="empty">
|
|
1367
|
+
Cannot connect to server<br>
|
|
1368
|
+
<span style="font-size:12px;color:#555;margin-top:8px;display:block">${esc(e.message)}</span>
|
|
1369
|
+
</div>`;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
init();
|
|
1374
|
+
})();
|
|
1375
|
+
</script>
|
|
1376
|
+
</body>
|
|
1377
|
+
</html>
|